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: Save detour creation snapshot to db #2729

Merged
merged 25 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
15d9028
Posts to db but author is null
hannahpurcell Jul 30, 2024
f9adc45
Save snapshot to db
hannahpurcell Aug 5, 2024
3f6038a
Formatting
hannahpurcell Aug 5, 2024
ab2556e
Use db id for detour as uuid instead of frontend-created one
hannahpurcell Aug 8, 2024
6b08b9a
formatting
hannahpurcell Aug 8, 2024
2fe30e6
Send not really needed in dependency array, but lint complained
hannahpurcell Aug 8, 2024
1daeac7
Fix event name to prioritize uuid
hannahpurcell Aug 12, 2024
a79b43a
Update putDetourUpdate to apiCallResult + useDetour snapshot subscrip…
hannahpurcell Aug 14, 2024
a398d08
Improve typing in new api call
hannahpurcell Aug 20, 2024
ca9f3fb
fix: Tweaked syntax in detours_controller
hannahpurcell Aug 20, 2024
6462a00
Include transitional state for saving-in-progress
hannahpurcell Aug 20, 2024
7142fa6
Formatting
hannahpurcell Aug 20, 2024
71ed919
fix: typo
hannahpurcell Aug 22, 2024
80d66f1
tweak: typing
hannahpurcell Aug 22, 2024
87e37ee
fix: typing correction
hannahpurcell Aug 22, 2024
fc7cb6c
tests!
hannahpurcell Aug 22, 2024
db14199
fix: typo!
hannahpurcell Aug 22, 2024
bf5483a
Merge branch 'main' into hp/test-branch-saving-to-db
hannahpurcell Aug 22, 2024
f1c3ea4
fix: formatting
hannahpurcell Aug 22, 2024
ffbccb8
fix: Split out tests
hannahpurcell Aug 22, 2024
8480c77
fix: redacted change
hannahpurcell Aug 22, 2024
de0f904
Missed some words in a comment!
hannahpurcell Aug 23, 2024
e2b84f1
Missed some words in second comment!
hannahpurcell Aug 23, 2024
b109fb8
fix: test adjustment
hannahpurcell Aug 23, 2024
8603420
Merge branch 'hp/test-branch-saving-to-db' of https://github.com/mbta…
hannahpurcell Aug 23, 2024
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
15 changes: 15 additions & 0 deletions assets/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import {
enums,
Infer,
is,
never,
number,
object,
Struct,
StructError,
Expand Down Expand Up @@ -62,6 +64,7 @@ import {
UnfinishedDetourData,
unfinishedDetourFromData,
} from "./models/unfinishedDetour"
import { Snapshot } from "xstate"

export interface RouteData {
id: string
Expand Down Expand Up @@ -441,6 +444,18 @@ export const putRouteTabs = (routeTabs: RouteTab[]): Promise<Response> =>
body: JSON.stringify({ route_tabs: routeTabs }),
})

export const putDetourUpdate = (
snapshot: Snapshot<unknown>
): Promise<Result<number, never>> =>
apiCallResult(`/api/detours/update_snapshot`, number(), never(), {
method: "PUT",
headers: {
"Content-Type": "application/json",
"x-csrf-token": getCsrfToken(),
},
body: JSON.stringify({ snapshot: snapshot }),
})

const getCsrfToken = (): string => appData()?.csrfToken || ""

export const nullableParser =
Expand Down
26 changes: 23 additions & 3 deletions assets/src/hooks/useDetour.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback } from "react"
import { ShapePoint } from "../schedule"
import { fetchUnfinishedDetour } from "../api"
import { fetchUnfinishedDetour, putDetourUpdate } from "../api"
import { useApiCall } from "./useApiCall"
import { isErr, isOk } from "../util/result"
import { useNearestIntersection } from "./useNearestIntersection"
Expand All @@ -27,9 +27,29 @@ export const useDetour = (input: UseDetourInput) => {

// Record snapshots when changed
useEffect(() => {
const snapshotSubscription = actorRef.subscribe(() => {
const serializedSnapshot = JSON.stringify(actorRef.getPersistedSnapshot())
const snapshotSubscription = actorRef.subscribe((snap) => {
const persistedSnapshot = actorRef.getPersistedSnapshot()
const serializedSnapshot = JSON.stringify(persistedSnapshot)
localStorage.setItem("snapshot", serializedSnapshot)

// check for no-save tag before
if (snap.hasTag("no-save")) {
return
}

actorRef.getSnapshot().can({ type: "detour.save.begin-save" }) &&
joshlarson marked this conversation as resolved.
Show resolved Hide resolved
actorRef.send({ type: "detour.save.begin-save" })

putDetourUpdate(persistedSnapshot).then((uuid) => {
if (
isOk(uuid) &&
actorRef
.getSnapshot()
.can({ type: "detour.save.set-uuid", uuid: uuid.ok })
) {
actorRef.send({ type: "detour.save.set-uuid", uuid: uuid.ok })
}
})
})

return () => {
Expand Down
45 changes: 43 additions & 2 deletions assets/src/models/createDetourMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { DetourShape, FinishedDetour } from "./detour"
export const createDetourMachine = setup({
types: {
context: {} as {
uuid: number | undefined
route?: Route
routePattern?: RoutePattern

Expand Down Expand Up @@ -61,7 +62,19 @@ export const createDetourMachine = setup({
| { type: "detour.edit.undo" }
| { type: "detour.share.copy-detour"; detourText: string }
| { type: "detour.share.activate" }
| { type: "detour.active.deactivate" },
| { type: "detour.active.deactivate" }
| { type: "detour.save.begin-save" }
| { type: "detour.save.set-uuid"; uuid: number },

// We're making an assumption that we'll never want to save detour edits to the database when in particular stages
// of detour drafting:
// -- when starting a detour, before any user input
// -- when the route id / route pattern is getting selected
// -- right after the route pattern is finalized, before any waypoints are added
// That leads to the following interface: if the user begins drafting a detour, adds waypoints, and then changes the route,
// the database will reflect the old route and old waypoints up until the point where a new waypoint is added.
// If that UX assumption isn't the right one, we can iterate in the future!
hannahpurcell marked this conversation as resolved.
Show resolved Hide resolved
tags: "no-save",
},
actors: {
"fetch-route-patterns": fromPromise<
Expand Down Expand Up @@ -161,18 +174,20 @@ export const createDetourMachine = setup({
context: ({ input }) => ({
...input,
waypoints: [],
uuid: undefined,
startPoint: undefined,
endPoint: undefined,
finishedDetour: undefined,
detourShape: undefined,
}),

type: "parallel",
initial: "Detour Drawing",
states: {
"Detour Drawing": {
initial: "Begin",
states: {
Begin: {
tags: "no-save",
always: [
{
guard: ({ context }) =>
Expand All @@ -185,6 +200,7 @@ export const createDetourMachine = setup({
},
"Pick Route Pattern": {
initial: "Pick Route ID",
tags: "no-save",
on: {
"detour.route-pattern.select-route": {
target: ".Pick Route ID",
Expand Down Expand Up @@ -295,6 +311,7 @@ export const createDetourMachine = setup({
},
states: {
"Pick Start Point": {
tags: "no-save",
on: {
"detour.edit.place-waypoint-on-route": {
target: "Place Waypoint",
Expand Down Expand Up @@ -428,6 +445,30 @@ export const createDetourMachine = setup({
Past: {},
},
},
SaveState: {
initial: "Unsaved",
states: {
Unsaved: {
on: {
"detour.save.begin-save": {
target: "Saving",
},
},
},
Saving: {
tags: "no-save",
on: {
"detour.save.set-uuid": {
target: "Saved",
actions: assign({
uuid: ({ event }) => event.uuid,
}),
},
},
},
Saved: {},
},
},
},
})

Expand Down
39 changes: 39 additions & 0 deletions assets/tests/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
fetchAllStops,
fetchFinishedDetour,
apiCallWithError,
putDetourUpdate,
} from "../src/api"
import routeFactory from "./factories/route"
import routeTabFactory from "./factories/routeTab"
Expand All @@ -50,6 +51,7 @@ import stopDataFactory from "./factories/stopData"
import { shapePointFactory } from "./factories/shapePointFactory"
import { ok, fetchError } from "../src/util/fetchResult"
import { directionsFactory } from "./factories/detourShapeFactory"
import { createActor, createMachine } from "xstate"

jest.mock("@sentry/react", () => ({
__esModule: true,
Expand All @@ -67,6 +69,7 @@ const mockFetch = (status: number, json: any): void => {
Promise.resolve({
json: () => json,
status,
ok: status == 200 ? true : false,
} as Response)
)
}
Expand Down Expand Up @@ -1122,3 +1125,39 @@ describe("putRouteTabs", () => {
expect(args!.body).toEqual(JSON.stringify({ route_tabs: routeTabs }))
})
})

describe("putDetourUpdate", () => {
const testToggleMachine = createMachine({
id: "testToggle",
initial: "inactive",
states: {
inactive: {
on: {
TOGGLE: "active",
},
},
active: {
on: {
TOGGLE: "inactive",
},
},
},
})
const actor = createActor(testToggleMachine).start()
const persistedSnapshot = actor.getPersistedSnapshot()

test("uses PUT and CSRF token", () => {
mockFetch(200, { data: 1 })
putDetourUpdate(persistedSnapshot)
expect(window.fetch).toHaveBeenCalledTimes(1)
expect(jest.mocked(window.fetch)).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
method: "PUT",
headers: expect.objectContaining({
"x-csrf-token": expect.any(String),
}),
})
)
})
})
140 changes: 140 additions & 0 deletions assets/tests/components/detours/diversionPage.saveDetour.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { jest, describe, test, expect, beforeEach } from "@jest/globals"
firestack marked this conversation as resolved.
Show resolved Hide resolved
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"
import React from "react"
import "@testing-library/jest-dom/jest-globals"
import { fetchRoutePatterns, putDetourUpdate } from "../../../src/api"
import {
DiversionPage as DiversionPageDefault,
DiversionPageProps,
} from "../../../src/components/detours/diversionPage"
import userEvent from "@testing-library/user-event"
import { originalRouteShape } from "../../testHelpers/selectors/components/detours/diversionPage"
import { Ok } from "../../../src/util/result"
import { neverPromise } from "../../testHelpers/mockHelpers"

import { originalRouteFactory } from "../../factories/originalRouteFactory"
import getTestGroups from "../../../src/userTestGroups"
import { routePatternFactory } from "../../factories/routePattern"
import { RoutesProvider } from "../../../src/contexts/routesContext"
import routeFactory from "../../factories/route"

const DiversionPage = (props: Partial<DiversionPageProps>) => {
return (
<DiversionPageDefault
originalRoute={originalRouteFactory.build()}
showConfirmCloseModal={false}
{...props}
/>
)
}

jest.mock("../../../src/api")
jest.mock("../../../src/userTestGroups")

beforeEach(() => {
jest.mocked(fetchRoutePatterns).mockReturnValue(neverPromise())
jest.mocked(getTestGroups).mockReturnValue([])
jest.mocked(putDetourUpdate).mockReturnValue(neverPromise())
})

describe("DiversionPage autosave flow", () => {
test("calls putDetourUpdate when the first waypoint is added", async () => {
const { container } = render(<DiversionPage />)

act(() => {
fireEvent.click(originalRouteShape.get(container))
})

act(() => {
fireEvent.click(container.querySelector(".c-vehicle-map")!)
})

await waitFor(() => {
expect(putDetourUpdate).toHaveBeenCalledTimes(1)
})
})

test("calls putDetourUpdate multiple times when a waypoint is added and then the detour is finished", async () => {
jest.mocked(putDetourUpdate).mockResolvedValue(Ok(12))

const { container } = render(<DiversionPage />)

act(() => {
fireEvent.click(originalRouteShape.get(container))
})

act(() => {
fireEvent.click(container.querySelector(".c-vehicle-map")!)
})

act(() => {
fireEvent.click(originalRouteShape.get(container))
})

await waitFor(() => {
expect(putDetourUpdate).toHaveBeenCalledTimes(3)
})
})

// We made an assumption that we'll never want to save detour edits in response to changing route/variant
test("when finish button is clicked, does not call API to update the database", async () => {
const route = routeFactory.build({ id: "66" })
const routePatterns = routePatternFactory.buildList(2, {
routeId: route.id,
})
const [rp1] = routePatterns

render(
<RoutesProvider routes={[route]}>
<DiversionPage
originalRoute={originalRouteFactory.build({
routePattern: rp1,
route,
})}
/>
</RoutesProvider>
)

await userEvent.click(
screen.getByRole("button", { name: "Change route or direction" })
)

await userEvent.click(
screen.getByRole("button", { name: "Start drawing detour" })
)

await waitFor(() => {
expect(putDetourUpdate).toHaveBeenCalledTimes(0)
})
})

// We made an assumption that we'll never want to save detour edits in response to changing route/variant
test("when route is changed, does not call API to update the database", async () => {
const routes = routeFactory.buildList(2)
const [route1, route2] = routes

render(
<RoutesProvider routes={routes}>
<DiversionPage
originalRoute={originalRouteFactory.build({
route: route1,
routePattern: routePatternFactory.build({ routeId: route1.id }),
})}
/>
</RoutesProvider>
)

await userEvent.click(
screen.getByRole("button", { name: "Change route or direction" })
)

await userEvent.selectOptions(
screen.getByRole("combobox", { name: "Choose route" }),
route2.name
)

await waitFor(() => {
expect(putDetourUpdate).toHaveBeenCalledTimes(0)
})
})
})
2 changes: 2 additions & 0 deletions assets/tests/components/detours/diversionPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
fetchNearestIntersection,
fetchRoutePatterns,
fetchUnfinishedDetour,
putDetourUpdate,
} from "../../../src/api"
import {
DiversionPage as DiversionPageDefault,
Expand Down Expand Up @@ -81,6 +82,7 @@ beforeEach(() => {
jest.mocked(fetchNearestIntersection).mockReturnValue(neverPromise())
jest.mocked(fetchRoutePatterns).mockReturnValue(neverPromise())
jest.mocked(getTestGroups).mockReturnValue([])
jest.mocked(putDetourUpdate).mockReturnValue(neverPromise())
})

describe("DiversionPage", () => {
Expand Down
Loading
Loading