Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion lib/dotcom_web/components/trip_planner/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ defmodule DotcomWeb.Components.TripPlanner.ItinerarySummary do
~H"""
<div class={@class}>
<div class="flex flex-row mb-3 font-bold text-lg justify-between">
<div>
<div data-test="itinerary_summary:time_range">
{formatted_time_range(@itinerary.start, @itinerary.end)}
<sup :if={Date.after?(@itinerary.end, @itinerary.start)}>+1</sup>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice import from our schedule finder redesign ✨

</div>
<div>
{@duration}
Expand Down
136 changes: 135 additions & 1 deletion test/dotcom_web/live/trip_planner_live_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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!

Expand Down Expand Up @@ -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.
Expand Down
23 changes: 23 additions & 0 deletions test/support/generators/date.ex
Original file line number Diff line number Diff line change
@@ -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