diff --git a/lib/dotcom_web/components/trip_planner/helpers.ex b/lib/dotcom_web/components/trip_planner/helpers.ex index 743adf5cf2..86c6bc33a8 100644 --- a/lib/dotcom_web/components/trip_planner/helpers.ex +++ b/lib/dotcom_web/components/trip_planner/helpers.ex @@ -144,12 +144,42 @@ defmodule DotcomWeb.Components.TripPlanner.Helpers do iex> formatted_time_range(~U[2025-08-15 09:41:17.283999Z], ~U[2025-08-15 09:58:47.283999Z]) "9:41\u2009–\u20099:58 am" + iex> formatted_time_range(~U[2025-08-15 09:41:17.283999Z], ~U[2025-08-15 12:58:47.283999Z]) + "9:41 am\u2009–\u200912:58 pm" + iex> Dotcom.Cldr.put_locale("es") ...> formatted_time_range(~U[2025-08-15 09:41:17.283999Z], ~U[2025-08-15 09:58:47.283999Z]) "9:41–9:58" + + > #### Potential Gotchas {: .error} + > + > If the inputs are `DateTime`'s, then this function strips the date info away before formatting + > the range. If you need an affordance to show that the end time is on a different day than the + > start time, the caller will have to take care of that. + > + > Also, this function will happily render a range that goes backwards in time. + + iex> formatted_time_range(~U[2025-08-15 23:41:17.283999Z], ~U[2025-08-16 00:38:47.283999Z]) + "11:41 pm\u2009–\u200912:38 am" + + iex> formatted_time_range(~U[2025-08-15 09:41:17.283999Z], ~U[2025-08-15 09:38:47.283999Z]) + "9:41\u2009–\u20099:38 am" """ def formatted_time_range(time1, time2) do - Dotcom.Cldr.Time.Interval.to_string!(time1, time2, format: :medium, period: :variant) + Dotcom.Cldr.Time.Interval.to_string!( + time1 |> to_time(), + time2 |> to_time(), + format: :medium, + period: :variant + ) + end + + defp to_time(%DateTime{} = date_time) do + DateTime.to_time(date_time) + end + + defp to_time(time) do + time end def formatted_times(times) do diff --git a/lib/dotcom_web/components/trip_planner/itinerary_summary.ex b/lib/dotcom_web/components/trip_planner/itinerary_summary.ex index 7c52fbdb6b..673297c152 100644 --- a/lib/dotcom_web/components/trip_planner/itinerary_summary.ex +++ b/lib/dotcom_web/components/trip_planner/itinerary_summary.ex @@ -55,8 +55,9 @@ defmodule DotcomWeb.Components.TripPlanner.ItinerarySummary do ~H"""
-
+
{formatted_time_range(@itinerary.start, @itinerary.end)} + +1
{@duration} diff --git a/test/dotcom_web/live/trip_planner_live_test.exs b/test/dotcom_web/live/trip_planner_live_test.exs index 3b6debb782..7a7363c2ea 100644 --- a/test/dotcom_web/live/trip_planner_live_test.exs +++ b/test/dotcom_web/live/trip_planner_live_test.exs @@ -3,10 +3,13 @@ defmodule DotcomWeb.TripPlannerLiveTest do import DotcomWeb.Router.Helpers, only: [live_path: 2, live_path: 3] import Mox - import Phoenix.LiveViewTest import OpenTripPlannerClient.Test.Support.Factory + import Phoenix.LiveViewTest alias Dotcom.TripPlan.AntiCorruptionLayer + alias Test.Support.Generators + + @timezone Application.compile_env!(:dotcom, :timezone) setup :verify_on_exit! @@ -467,6 +470,137 @@ defmodule DotcomWeb.TripPlannerLiveTest do assert Floki.find(document, "div[data-test='itinerary_detail:selected:1']") != [] end + + test "renders time range as 'h:mm - h:mm am' if both times are in the morning", %{view: view} do + date = Generators.Date.random_date() + + [start_time, end_time] = + Faker.Util.sample_uniq(2, fn -> + Generators.DateTime.random_time_range_date_time( + {DateTime.new!(date, ~T[00:00:00], @timezone), + DateTime.new!(date, ~T[11:59:59], @timezone)} + ) + end) + |> Enum.sort(DateTime) + + # Setup + expect(OpenTripPlannerClient.Mock, :plan, fn _ -> + {:ok, [itinerary_group_with_time_range(start_time, end_time)]} + end) + + # Exercise + view |> element("form") |> render_change(%{"input_form" => @valid_params}) + + # Verify + assert rendered_time_range(view) == + "#{pretty_time(start_time)}\u2009–\u2009#{pretty_time(end_time)} am" + end + + test "renders time range as 'h:mm - h:mm pm' if both times are after noon", %{view: view} do + date = Generators.Date.random_date() + + [start_time, end_time] = + Faker.Util.sample_uniq(2, fn -> + Generators.DateTime.random_time_range_date_time( + {DateTime.new!(date, ~T[12:00:00], @timezone), + DateTime.new!(date, ~T[23:59:59], @timezone)} + ) + end) + |> Enum.sort(DateTime) + + # Setup + expect(OpenTripPlannerClient.Mock, :plan, fn _ -> + {:ok, [itinerary_group_with_time_range(start_time, end_time)]} + end) + + # Exercise + view |> element("form") |> render_change(%{"input_form" => @valid_params}) + + # Verify + assert rendered_time_range(view) == + "#{pretty_time(start_time)}\u2009–\u2009#{pretty_time(end_time)} pm" + end + + test "renders time range as 'h:mm am - h:mm pm' if the itinerary crosses noon", %{view: view} do + date = Generators.Date.random_date() + + start_time = + Generators.DateTime.random_time_range_date_time( + {DateTime.new!(date, ~T[00:00:00], @timezone), + DateTime.new!(date, ~T[11:59:59], @timezone)} + ) + + end_time = + Generators.DateTime.random_time_range_date_time( + {DateTime.new!(date, ~T[12:00:00], @timezone), + DateTime.new!(date, ~T[23:59:59], @timezone)} + ) + + # Setup + expect(OpenTripPlannerClient.Mock, :plan, fn _ -> + {:ok, [itinerary_group_with_time_range(start_time, end_time)]} + end) + + # Exercise + view |> element("form") |> render_change(%{"input_form" => @valid_params}) + + # Verify + assert rendered_time_range(view) == + "#{pretty_time(start_time)} am\u2009–\u2009#{pretty_time(end_time)} pm" + end + + test "renders time range as 'h:mm pm - h:mm am +1' if the itinerary crosses a day boundary", + %{view: view} do + date = Generators.Date.random_date() + next_date = date |> Date.shift(day: 1) + + start_time = + Generators.DateTime.random_time_range_date_time( + {DateTime.new!(date, ~T[12:00:00], @timezone), + DateTime.new!(date, ~T[23:59:59], @timezone)} + ) + + end_time = + Generators.DateTime.random_time_range_date_time( + {DateTime.new!(next_date, ~T[00:00:00], @timezone), + DateTime.new!(next_date, ~T[11:59:59], @timezone)} + ) + + # Setup + expect(OpenTripPlannerClient.Mock, :plan, fn _ -> + {:ok, [itinerary_group_with_time_range(start_time, end_time)]} + end) + + # Exercise + view |> element("form") |> render_change(%{"input_form" => @valid_params}) + + # Verify + assert rendered_time_range(view) == + "#{pretty_time(start_time)} pm\u2009–\u2009#{pretty_time(end_time)} am +1" + end + end + + defp itinerary_group_with_time_range(start_time, end_time) do + build(:itinerary_group, + representative_index: 0, + itineraries: [build(:itinerary, start: start_time, end: end_time)] + ) + end + + defp rendered_time_range(view) do + view + |> render_async() + |> Floki.parse_document!() + |> Floki.get_by_id("trip-planner-results") + |> Floki.find("[data-test=\"itinerary_summary:time_range\"") + |> Floki.text() + |> String.trim() + |> String.split("\n") + |> Enum.map_join(" ", &String.trim/1) + end + + defp pretty_time(date_time) do + date_time |> Cldr.DateTime.to_string!(Dotcom.Cldr, format: "h:mm") end # Parse coordinates from data-coordinates. diff --git a/test/support/generators/date.ex b/test/support/generators/date.ex new file mode 100644 index 0000000000..44828591ce --- /dev/null +++ b/test/support/generators/date.ex @@ -0,0 +1,23 @@ +defmodule Test.Support.Generators.Date do + @moduledoc """ + Factories to help generate/evaluate dates for testing. + """ + + @date_time_module Application.compile_env!(:dotcom, :date_time_module) + + @doc "Generate a random date this decade" + def random_date() do + now = @date_time_module.now() + + beginning_of_year = Date.new!(now.year, 1, 1) + end_of_year = Date.new!(now.year + 1, 1, 1) + + date_generator_between({beginning_of_year, end_of_year}) |> Enum.take(1) |> List.first() + end + + def date_generator_between({start, stop}) do + StreamData.repeatedly(fn -> + Faker.Date.between(start, stop) + end) + end +end