From aae9dda624fcd8e6870b8ddc9cd21ddbafd6fef6 Mon Sep 17 00:00:00 2001 From: Josh Larson Date: Sat, 7 Feb 2026 10:46:05 -0500 Subject: [PATCH 1/5] tests(TripPlanner): Add tests for time-range rendering --- .../trip_planner/itinerary_summary.ex | 2 +- .../live/trip_planner_live_test.exs | 104 +++++++++++++++++- test/support/generators/date.ex | 19 ++++ 3 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 test/support/generators/date.ex diff --git a/lib/dotcom_web/components/trip_planner/itinerary_summary.ex b/lib/dotcom_web/components/trip_planner/itinerary_summary.ex index 7c52fbdb6b..0f236722e8 100644 --- a/lib/dotcom_web/components/trip_planner/itinerary_summary.ex +++ b/lib/dotcom_web/components/trip_planner/itinerary_summary.ex @@ -55,7 +55,7 @@ defmodule DotcomWeb.Components.TripPlanner.ItinerarySummary do ~H"""
-
+
{formatted_time_range(@itinerary.start, @itinerary.end)}
diff --git a/test/dotcom_web/live/trip_planner_live_test.exs b/test/dotcom_web/live/trip_planner_live_test.exs index 3b6debb782..8ad3a18032 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,105 @@ 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 + 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() + 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..4defc713c2 --- /dev/null +++ b/test/support/generators/date.ex @@ -0,0 +1,19 @@ +defmodule Test.Support.Generators.Date do + @moduledoc """ + Factories to help generate/evaluate dates for testing. + """ + + @doc "Generate a random date this decade" + def random_date() do + beginning_of_time = ~N[2020-01-01 00:00:00] + end_of_time = ~N[2029-12-31 23:59:59] + + date_generator_between({beginning_of_time, end_of_time}) |> Enum.take(1) |> List.first() + end + + def date_generator_between({start, stop}) do + StreamData.repeatedly(fn -> + Faker.Date.between(start, stop) + end) + end +end From e01d66452e9346a034078e3352576a5c7f3d6e62 Mon Sep 17 00:00:00 2001 From: Josh Larson Date: Mon, 9 Feb 2026 08:45:01 -0500 Subject: [PATCH 2/5] fix(TripPlanner): Render time range correctly across midnight boundary --- .../components/trip_planner/helpers.ex | 32 ++++++++++++++++++- .../live/trip_planner_live_test.exs | 30 +++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) 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/test/dotcom_web/live/trip_planner_live_test.exs b/test/dotcom_web/live/trip_planner_live_test.exs index 8ad3a18032..fafe0caf19 100644 --- a/test/dotcom_web/live/trip_planner_live_test.exs +++ b/test/dotcom_web/live/trip_planner_live_test.exs @@ -548,6 +548,36 @@ defmodule DotcomWeb.TripPlannerLiveTest do 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' 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" + end end defp itinerary_group_with_time_range(start_time, end_time) do From c82e9f539d7b3781790b9e9d84d23ab53cfbcca5 Mon Sep 17 00:00:00 2001 From: Josh Larson Date: Sat, 7 Feb 2026 10:51:31 -0500 Subject: [PATCH 3/5] tweak(TripPlanner): Show `+1` superscript if itinerary crosses a day boundary --- .../components/trip_planner/itinerary_summary.ex | 1 + test/dotcom_web/live/trip_planner_live_test.exs | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/dotcom_web/components/trip_planner/itinerary_summary.ex b/lib/dotcom_web/components/trip_planner/itinerary_summary.ex index 0f236722e8..673297c152 100644 --- a/lib/dotcom_web/components/trip_planner/itinerary_summary.ex +++ b/lib/dotcom_web/components/trip_planner/itinerary_summary.ex @@ -57,6 +57,7 @@ defmodule DotcomWeb.Components.TripPlanner.ItinerarySummary do
{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 fafe0caf19..a53bf9d878 100644 --- a/test/dotcom_web/live/trip_planner_live_test.exs +++ b/test/dotcom_web/live/trip_planner_live_test.exs @@ -549,7 +549,7 @@ defmodule DotcomWeb.TripPlannerLiveTest do "#{pretty_time(start_time)} am\u2009–\u2009#{pretty_time(end_time)} pm" end - test "renders time range as 'h:mm pm - h:mm am' if the itinerary crosses a day boundary", + 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) @@ -576,7 +576,7 @@ defmodule DotcomWeb.TripPlannerLiveTest do # Verify assert rendered_time_range(view) == - "#{pretty_time(start_time)} pm\u2009–\u2009#{pretty_time(end_time)} am" + "#{pretty_time(start_time)} pm\u2009–\u2009#{pretty_time(end_time)} am +1" end end @@ -595,6 +595,9 @@ defmodule DotcomWeb.TripPlannerLiveTest do |> Floki.find("[data-test=\"itinerary_summary:time_range\"") |> Floki.text() |> String.trim() + |> String.split("\n") + |> Enum.map(&String.trim/1) + |> Enum.join(" ") end defp pretty_time(date_time) do From 0cbd28f66c94e749f1b8deead3ccc2186284b6d8 Mon Sep 17 00:00:00 2001 From: Josh Larson Date: Mon, 9 Feb 2026 09:47:57 -0500 Subject: [PATCH 4/5] linting: Use `Enum.map_join/3` --- test/dotcom_web/live/trip_planner_live_test.exs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/dotcom_web/live/trip_planner_live_test.exs b/test/dotcom_web/live/trip_planner_live_test.exs index a53bf9d878..7a7363c2ea 100644 --- a/test/dotcom_web/live/trip_planner_live_test.exs +++ b/test/dotcom_web/live/trip_planner_live_test.exs @@ -596,8 +596,7 @@ defmodule DotcomWeb.TripPlannerLiveTest do |> Floki.text() |> String.trim() |> String.split("\n") - |> Enum.map(&String.trim/1) - |> Enum.join(" ") + |> Enum.map_join(" ", &String.trim/1) end defp pretty_time(date_time) do From cffd0af86dfb81bf9f5664012095a52e617a783a Mon Sep 17 00:00:00 2001 From: Josh Larson Date: Tue, 10 Feb 2026 14:46:13 -0500 Subject: [PATCH 5/5] feedback: Use `beginning_of_year`/`end_of_year` instead of hard-coded dates --- test/support/generators/date.ex | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/support/generators/date.ex b/test/support/generators/date.ex index 4defc713c2..44828591ce 100644 --- a/test/support/generators/date.ex +++ b/test/support/generators/date.ex @@ -3,12 +3,16 @@ defmodule Test.Support.Generators.Date do 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 - beginning_of_time = ~N[2020-01-01 00:00:00] - end_of_time = ~N[2029-12-31 23:59:59] + 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_time, end_of_time}) |> Enum.take(1) |> List.first() + date_generator_between({beginning_of_year, end_of_year}) |> Enum.take(1) |> List.first() end def date_generator_between({start, stop}) do