diff --git a/lib/dotcom_web/components/live_components/trip_planner_results_section.ex b/lib/dotcom_web/components/live_components/trip_planner_results_section.ex new file mode 100644 index 0000000000..dd1c9c68ef --- /dev/null +++ b/lib/dotcom_web/components/live_components/trip_planner_results_section.ex @@ -0,0 +1,99 @@ +defmodule DotcomWeb.Components.LiveComponents.TripPlannerResultsSection do + @moduledoc """ + The section of the trip planner page that shows the map and + the summary or details panel + """ + + use DotcomWeb, :live_component + + import DotcomWeb.Components.TripPlanner.ItineraryDetail + import DotcomWeb.Components.TripPlanner.ItineraryGroup, only: [itinerary_group: 1] + + @impl true + def mount(socket) do + {:ok, socket |> assign(:expanded_itinerary_index, nil)} + end + + @impl true + def render(assigns) do + ~H""" +
+
+ <%= inspect(@error) %> +
+ <.async_result :let={results} assign={@results}> +
+ <.itinerary_panel + results={results} + details_index={@expanded_itinerary_index} + target={@myself} + /> +
+ + <.live_component + module={MbtaMetro.Live.Map} + id="trip-planner-map" + class="h-96 w-full relative overflow-none" + config={@map_config} + pins={[@from, @to]} + /> +
+ """ + end + + defp itinerary_panel(%{results: results, details_index: details_index} = assigns) do + case details_index do + nil -> + ~H""" + <.itinerary_panel_with_all_results results={@results} target={@target} /> + """ + + _ -> + assigns = assign(assigns, :result, Enum.at(results, details_index)) + + ~H""" + <.itinerary_panel_with_specific_result result={@result} target={@target} /> + """ + end + end + + defp itinerary_panel_with_all_results(assigns) do + ~H""" + <.itinerary_group + :for={{result, index} <- Enum.with_index(@results)} + index={index} + details_click_event="show_itinerary_details" + target={@target} + {result} + /> + """ + end + + defp itinerary_panel_with_specific_result(%{result: result} = assigns) do + assigns = assign(assigns, :itineraries, Map.get(result, :itineraries)) + + ~H""" +
+ + <.itinerary_detail :for={itinerary <- @itineraries} itinerary={itinerary} /> +
+ """ + end + + @impl true + def handle_event("show_itinerary_details", %{"index" => index_str}, socket) do + {index, ""} = Integer.parse(index_str) + + {:noreply, socket |> assign(:expanded_itinerary_index, index)} + end + + @impl true + def handle_event("show_itinerary_summary", _params, socket) do + {:noreply, socket |> assign(:expanded_itinerary_index, nil)} + end +end diff --git a/lib/dotcom_web/components/trip_planner/itinerary_group.ex b/lib/dotcom_web/components/trip_planner/itinerary_group.ex index 5dafdc127d..fa60ba30ed 100644 --- a/lib/dotcom_web/components/trip_planner/itinerary_group.ex +++ b/lib/dotcom_web/components/trip_planner/itinerary_group.ex @@ -4,18 +4,24 @@ defmodule DotcomWeb.Components.TripPlanner.ItineraryGroup do """ use DotcomWeb, :component - import DotcomWeb.Components.TripPlanner.ItineraryDetail - attr(:summary, :map, doc: "ItineraryGroups.summary()", required: true) attr(:itineraries, :list, doc: "List of %Dotcom.TripPlan.Itinerary{}", required: true) + attr(:index, :integer, + doc: "Index into the full list where this itinerary group sits", + required: true + ) + + attr :target, :string, doc: "The target that should receive events", required: true + + attr :details_click_event, :string, + doc: "The event that fires when 'Details' is clicked", + required: true + @doc """ Renders a single itinerary group. """ def itinerary_group(assigns) do - assigns = - assign(assigns, :group_id, "group-#{:erlang.phash2(assigns.itineraries)}") - ~H"""
Enum.join(", ") %>
-
- """ end diff --git a/lib/dotcom_web/live/trip_planner.ex b/lib/dotcom_web/live/trip_planner.ex index ad9462ff08..eafe7946af 100644 --- a/lib/dotcom_web/live/trip_planner.ex +++ b/lib/dotcom_web/live/trip_planner.ex @@ -7,10 +7,9 @@ defmodule DotcomWeb.Live.TripPlanner do use DotcomWeb, :live_view - import DotcomWeb.Components.TripPlanner.ItineraryGroup, only: [itinerary_group: 1] import MbtaMetro.Components.{Feedback, Spinner} - alias DotcomWeb.Components.LiveComponents.TripPlannerForm + alias DotcomWeb.Components.LiveComponents.{TripPlannerForm, TripPlannerResultsSection} alias Dotcom.TripPlan.{AntiCorruptionLayer, InputForm.Modes, ItineraryGroups} @form_id "trip-planner-form" @@ -67,23 +66,16 @@ defmodule DotcomWeb.Live.TripPlanner do <% end %> -
-
- <%= inspect(@error) %> -
- <.async_result :let={results} assign={@results}> -
- <.itinerary_group :for={result <- results} {result} /> -
- - <.live_component - module={MbtaMetro.Live.Map} - id="trip-planner-map" - class="h-96 w-full relative overflow-none" - config={@map_config} - pins={[@from, @to]} - /> -
+ + <.live_component + module={TripPlannerResultsSection} + id="trip-planner-results" + results={@results} + error={@error} + map_config={@map_config} + from={@from} + to={@to} + /> """ end diff --git a/test/dotcom_web/live/trip_planner_test.exs b/test/dotcom_web/live/trip_planner_test.exs index 84d3347728..9e5f929d87 100644 --- a/test/dotcom_web/live/trip_planner_test.exs +++ b/test/dotcom_web/live/trip_planner_test.exs @@ -1,8 +1,11 @@ defmodule DotcomWeb.Live.TripPlannerTest do use DotcomWeb.ConnCase, async: true + import Mox import Phoenix.LiveViewTest + setup :verify_on_exit! + test "Preview version behind basic auth", %{conn: conn} do conn = get(conn, ~p"/preview/trip-planner") @@ -67,4 +70,75 @@ defmodule DotcomWeb.Live.TripPlannerTest do # test "pushes updated location to the map", %{view: view} do # end end + + describe "Trip Planner with results" do + setup %{conn: conn} do + [username: username, password: password] = + Application.get_env(:dotcom, DotcomWeb.Router)[:basic_auth] + + %{ + conn: + put_req_header( + conn, + "authorization", + "Basic " <> Base.encode64("#{username}:#{password}") + ) + } + end + + test "no results", %{conn: conn} do + params = %{ + "plan" => %{ + "from_latitude" => "#{Faker.Address.latitude()}", + "from_longitude" => "#{Faker.Address.longitude()}", + "to_latitude" => "#{Faker.Address.latitude()}", + "to_longitude" => "#{Faker.Address.longitude()}" + } + } + + expect(OpenTripPlannerClient.Mock, :plan, fn _ -> + {:ok, %OpenTripPlannerClient.Plan{itineraries: []}} + end) + + {:ok, view, _html} = live(conn, ~p"/preview/trip-planner?#{params}") + + # TODO actually wait for the async_result somehow + Process.sleep(1000) + + assert render(view) =~ "No trips found" + end + + test "with results", %{conn: conn} do + params = %{ + "plan" => %{ + "from_latitude" => "#{Faker.Address.latitude()}", + "from_longitude" => "#{Faker.Address.longitude()}", + "to_latitude" => "#{Faker.Address.latitude()}", + "to_longitude" => "#{Faker.Address.longitude()}" + } + } + + # called during itinerary parsing + stub(Stops.Repo.Mock, :get, fn _ -> + Test.Support.Factories.Stops.Stop.build(:stop) + end) + + # Uhhh the OTP factory will generate with any route_type value but our + # parsing will break with unexpected route types + itineraries = + OpenTripPlannerClient.Test.Support.Factory.build_list(3, :itinerary) + |> Enum.map(&Test.Support.Factories.TripPlanner.TripPlanner.limit_route_types/1) + + expect(OpenTripPlannerClient.Mock, :plan, fn _ -> + {:ok, %OpenTripPlannerClient.Plan{itineraries: itineraries}} + end) + + {:ok, view, _html} = live(conn, ~p"/preview/trip-planner?#{params}") + + # TODO actually wait for the async_result somehow + Process.sleep(1000) + + refute render(view) =~ "No trips found" + end + end end diff --git a/test/support/factories/trip_planner/trip_planner.ex b/test/support/factories/trip_planner/trip_planner.ex index 62c6cd890c..8c8858cd79 100644 --- a/test/support/factories/trip_planner/trip_planner.ex +++ b/test/support/factories/trip_planner/trip_planner.ex @@ -141,22 +141,22 @@ defmodule Test.Support.Factories.TripPlanner.TripPlanner do # OpenTripPlannerClient supports a greater number of route_type values than # Dotcom does! Tweak that here. - defp limit_route_types(%OpenTripPlannerClient.Schema.Itinerary{legs: legs} = itinerary) do + def limit_route_types(%OpenTripPlannerClient.Schema.Itinerary{legs: legs} = itinerary) do %OpenTripPlannerClient.Schema.Itinerary{ itinerary | legs: Enum.map(legs, &limit_route_types/1) } end - defp limit_route_types(%OpenTripPlannerClient.Schema.Leg{route: route} = leg) - when route.type > 4 do + def limit_route_types(%OpenTripPlannerClient.Schema.Leg{route: route} = leg) + when route.type > 4 do %OpenTripPlannerClient.Schema.Leg{ leg | route: %OpenTripPlannerClient.Schema.Route{route | type: Faker.Util.pick([0, 1, 2, 3, 4])} } end - defp limit_route_types(leg), do: leg + def limit_route_types(leg), do: leg def stop_named_position_factory do %NamedPosition{