diff --git a/assets/css/_autocomplete-theme.scss b/assets/css/_autocomplete-theme.scss index e8229714c6..d972c64eff 100644 --- a/assets/css/_autocomplete-theme.scss +++ b/assets/css/_autocomplete-theme.scss @@ -227,7 +227,6 @@ .aa-InputWrapperSuffix { order: 3; - width: calc(var(--aa-spacing) + var(--aa-icon-size) - 1px); } // hide default search magnifying glass icon @@ -252,6 +251,12 @@ content: "B" } -.aa-DetachedOverlay .aa-InputWrapper { - padding-left: calc(var(--aa-spacing)); +.aa-DetachedOverlay { + .aa-InputWrapper { + padding-left: calc(var(--aa-spacing)); + } + + .aa-InputWrapperSuffix { + width: calc(var(--aa-spacing) + var(--aa-icon-size) - 1px); + } } diff --git a/cypress/e2e/smoke.cy.js b/cypress/e2e/smoke.cy.js index 7416723d32..3de6db722a 100644 --- a/cypress/e2e/smoke.cy.js +++ b/cypress/e2e/smoke.cy.js @@ -152,30 +152,6 @@ describe("passes smoke test", () => { } }); - it("trip planner", () => { - cy.visit("/trip-planner"); - - // reverses the inputs - cy.get("#from").type("A"); - cy.get("#to").type("B"); - cy.get("#trip-plan-reverse-control").click(); - cy.get("#from").should("have.value", "B"); - cy.get("#to").should("have.value", "A"); - - // opens the date picker - cy.contains("#trip-plan-datepicker").should("not.exist"); - cy.get('label[for="arrive"]').click(); - cy.get("#trip-plan-datepicker"); - - // shortcut /from/ - marker A prepopulated - cy.visit("/trip-planner/from/North+Station"); - cy.get('img.leaflet-marker-icon[src="/icon-svg/icon-map-pin-a.svg"]'); - - // shortcut /to/ - marker B prepopulated - cy.visit("/trip-planner/to/North+Station"); - cy.get('img.leaflet-marker-icon[src="/icon-svg/icon-map-pin-b.svg"]'); - }); - it("alerts page", () => { cy.visit("/alerts"); cy.contains(".m-alerts__mode-buttons a", "Bus").click(); diff --git a/integration/scenarios/plan-a-trip-from-homepage.js b/integration/scenarios/plan-a-trip-from-homepage.js index b4a241e98f..92bb0c83e7 100644 --- a/integration/scenarios/plan-a-trip-from-homepage.js +++ b/integration/scenarios/plan-a-trip-from-homepage.js @@ -29,7 +29,7 @@ exports.scenario = async ({ page, baseURL }) => { await expect .poll(async () => - page.locator("div.m-trip-plan-results__itinerary").count(), + page.locator("section#trip-planner-results").count(), ) .toBeGreaterThan(0); }; diff --git a/integration/scenarios/plan-a-trip.js b/integration/scenarios/plan-a-trip.js index 4473a476b1..6fc2756046 100644 --- a/integration/scenarios/plan-a-trip.js +++ b/integration/scenarios/plan-a-trip.js @@ -3,29 +3,33 @@ const { expect } = require("@playwright/test"); exports.scenario = async ({ page, baseURL }) => { await page.goto(`${baseURL}/trip-planner`); - await page.locator("input#from").pressSequentially("North Station"); + await expect( + page.getByRole("heading", { name: "Trip Planner" }), + ).toBeVisible(); + + await page.locator("#trip-planner-input-form--from input[type='search']").pressSequentially("North Station"); await page.waitForSelector( - "div#from-autocomplete-results span.c-search-bar__-dropdown-menu", + "ul.aa-List", ); await page.keyboard.press("ArrowDown"); await page.keyboard.press("Enter"); - await page.locator("input#to").pressSequentially("South Station"); + // The A location pin. + await page.waitForSelector("#mbta-metro-pin-0"); + + await page.locator("#trip-planner-input-form--to input[type='search']").pressSequentially("South Station"); await page.waitForSelector( - "div#to-autocomplete-results span.c-search-bar__-dropdown-menu", + "ul.aa-List", ); await page.keyboard.press("ArrowDown"); await page.keyboard.press("Enter"); - await page.locator("button#trip-plan__submit").click(); - - await expect( - page.getByRole("heading", { name: "Trip Planner" }), - ).toBeVisible(); + // The B location pin. + await page.waitForSelector("#mbta-metro-pin-1"); await expect .poll(async () => - page.locator("div.m-trip-plan-results__itinerary").count(), + page.locator("section#trip-planner-results").count(), ) .toBeGreaterThan(0); }; diff --git a/lib/dotcom/trip_plan/anti_corruption_layer.ex b/lib/dotcom/trip_plan/anti_corruption_layer.ex index ab907c7414..5b3052db1e 100644 --- a/lib/dotcom/trip_plan/anti_corruption_layer.ex +++ b/lib/dotcom/trip_plan/anti_corruption_layer.ex @@ -8,36 +8,72 @@ defmodule Dotcom.TripPlan.AntiCorruptionLayer do We ignore datetime_type and datetime and allow those to be set to 'now' and the current time respectively. """ - @location_service Application.compile_env!(:dotcom, :location_service) + @default_modes Dotcom.TripPlan.InputForm.initial_modes() + @default_params %{ + "datetime_type" => "now", + "modes" => @default_modes, + "wheelchair" => "false" + } @doc """ - Given a query for the old trip planner /to or /from actions, replicate the old - behavior by searching for a location and using the first result. Convert this - to the new trip planner form values. + Given the params from the old trip planner, convert them to the new trip planner form values. + + If no plan is given, then we default to empty form values. """ - def convert_old_action(action) do - with [key] when key in [:from, :to] <- Map.keys(action), - query when is_binary(query) <- Map.get(action, key), - {:ok, [%LocationService.Address{} = geocoded | _]} <- @location_service.geocode(query) do - %{ - "plan" => %{ - "#{key}_latitude" => geocoded.latitude, - "#{key}_longitude" => geocoded.longitude, - "#{key}" => geocoded.formatted - } - } + def convert_old_params(%{"plan" => params}) do + Map.merge(@default_params, copy_params(params)) + end + + def convert_old_params(_), do: convert_old_params(%{"plan" => %{}}) + + # Decode a string into form values. + # Add in defaults if they were omitted. + def decode(string) do + with {:ok, binary} <- Base.url_decode64(string), + {:ok, params} <- :msgpack.unpack(binary) do + params + |> decode_datetime() + |> add_defaults() else - _ -> - %{"plan" => %{}} + _ -> @default_params end end - @doc """ - Given the params from the old trip planner, convert them to the new trip planner form values. + def default_params, do: @default_params - If no plan is given, then we default to empty form values. - """ - def convert_old_params(%{"plan" => params}) do + # Encode form values into a single string. + # Strip out defaults so we don't waste space encoding them. + def encode(params) do + params + |> strip_defaults() + |> encode_datetime() + |> :msgpack.pack() + |> Base.url_encode64() + end + + # Make sure that the params have all of the defaults set. + defp add_defaults(params) do + Map.merge(@default_params, params) + end + + # All other modes but commuter rail are just the mode in uppercase. + defp convert_mode("commuter_rail"), do: "RAIL" + defp convert_mode(mode), do: String.upcase(mode) + + # When modes are given, we set all non-given modes to false. + defp convert_modes(modes) when is_map(modes) do + default_modes = for {k, _} <- @default_modes, into: %{}, do: {k, "false"} + + Enum.reduce(modes, default_modes, fn {key, value}, acc -> + Map.put(acc, convert_mode(key), value) + end) + end + + # When no modes are given, we use the initial modes--all modes are true. + defp convert_modes(_), do: @default_modes + + # Copy the old params into the new param structure. + defp copy_params(params) do %{ "from" => %{ "latitude" => Map.get(params, "from_latitude"), @@ -56,20 +92,34 @@ defmodule Dotcom.TripPlan.AntiCorruptionLayer do } end - def convert_old_params(_), do: convert_old_params(%{"plan" => %{}}) + defp decode_datetime(%{"datetime" => datetime} = params) do + case DateTime.from_iso8601(datetime) do + {:ok, datetime, _} -> Map.put(params, "datetime", datetime) + _ -> params + end + end - defp convert_modes(modes) when is_map(modes) do - default_modes = - for {k, _} <- Dotcom.TripPlan.InputForm.initial_modes(), into: %{}, do: {k, "false"} + defp decode_datetime(params), do: params - modes - |> Enum.reduce(default_modes, fn {key, value}, acc -> - Map.put(acc, convert_mode(key), value) - end) + # Encode the datetime into an ISO8601 string. + defp encode_datetime(params) do + case params |> Map.get("datetime") do + %DateTime{} = datetime -> Map.put(params, "datetime", DateTime.to_iso8601(datetime)) + _ -> params + end end - defp convert_modes(_), do: Dotcom.TripPlan.InputForm.initial_modes() + # If the params have a key set and it's just the default value, then remove it. + defp strip_default(params, key) do + if Map.has_key?(params, key) && Map.get(params, key) == Map.get(@default_params, key) do + Map.delete(params, key) + else + params + end + end - defp convert_mode("commuter_rail"), do: "RAIL" - defp convert_mode(mode), do: String.upcase(mode) + # Strip default params so we don't waste space encoding them. + defp strip_defaults(params) do + Enum.reduce(@default_params, params, fn {key, _}, acc -> strip_default(acc, key) end) + end end diff --git a/lib/dotcom/trip_plan/input_form.ex b/lib/dotcom/trip_plan/input_form.ex index 5623f9544a..f6c8bf5302 100644 --- a/lib/dotcom/trip_plan/input_form.ex +++ b/lib/dotcom/trip_plan/input_form.ex @@ -24,7 +24,7 @@ defmodule Dotcom.TripPlan.InputForm do embeds_one(:modes, __MODULE__.Modes) field(:datetime_type, :string) field(:datetime, :naive_datetime) - field(:wheelchair, :boolean, default: true) + field(:wheelchair, :boolean) end def initial_modes do diff --git a/lib/dotcom/trip_plan/parser.ex b/lib/dotcom/trip_plan/parser.ex index 146296e340..44b36a8109 100644 --- a/lib/dotcom/trip_plan/parser.ex +++ b/lib/dotcom/trip_plan/parser.ex @@ -10,6 +10,8 @@ defmodule Dotcom.TripPlan.Parser do MBTA system. """ + require Logger + alias Dotcom.TripPlan.{FarePasses, Itinerary, Leg, NamedPosition, PersonalDetail, TransitDetail} alias OpenTripPlannerClient.Schema @@ -164,20 +166,30 @@ defmodule Dotcom.TripPlan.Parser do defp route_color("Logan Express", "DV", _), do: "704c9f" defp route_color(_, _, color), do: color - defp build_stop(stop, attributes \\ %{}) do - case stop.gtfs_id do - "mbta-ma-us:" <> gtfs_id -> - @stops_repo.get(gtfs_id) - |> struct(attributes) - - _ -> - stop - |> Map.from_struct() - |> Map.merge(attributes) - |> then(&struct(Stops.Stop, &1)) + defp build_stop(stop, attributes \\ %{}) + + defp build_stop(%Schema.Stop{gtfs_id: "mbta-ma-us:" <> gtfs_id} = schema_stop, attributes) do + stop = @stops_repo.get(gtfs_id) + + if stop do + stop + |> Map.merge(attributes) + else + Logger.notice("dotcom.trip_plan.parser unknown_stop=mbta-ma-us:#{gtfs_id}") + + schema_stop + |> Map.put(:gtfs_id, gtfs_id) + |> build_stop(attributes) end end + defp build_stop(stop, attributes) do + stop + |> Map.from_struct() + |> Map.merge(attributes) + |> then(&struct(Stops.Stop, &1)) + end + defp id_from_gtfs(gtfs_id) do case String.split(gtfs_id, ":") do [_, id] -> id diff --git a/lib/dotcom_web/components/trip_planner/results.ex b/lib/dotcom_web/components/trip_planner/results.ex index cea62d3f3f..7291ebebf2 100644 --- a/lib/dotcom_web/components/trip_planner/results.ex +++ b/lib/dotcom_web/components/trip_planner/results.ex @@ -22,6 +22,7 @@ defmodule DotcomWeb.Components.TripPlanner.Results do
0 && @results.itinerary_group_selection} class="h-min w-full mb-3.5" + data-test={"results:itinerary_group:selected:#{@results.itinerary_group_selection}"} >