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(", ") %>
-
-
- <.itinerary_detail :for={itinerary <- @itineraries} itinerary={itinerary} />
-
"""
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{