diff --git a/assets/src/components/admin/inspector.tsx b/assets/src/components/admin/inspector.tsx
index 16da9187c..c7a103a05 100644
--- a/assets/src/components/admin/inspector.tsx
+++ b/assets/src/components/admin/inspector.tsx
@@ -32,6 +32,10 @@ const APP_IDS = new Set(Object.keys(SCREEN_APPS));
const MAX_SSML_BILLED_CHARS = 3000;
const MAX_SSML_TOTAL_CHARS = 6000;
+const anyVariantsExist = Object.values(SCREEN_APPS).some(
+ ({ variants }) => variants.length > 0,
+);
+
const buildIframeUrl = (
screen: ScreenWithId | null,
isSimulation: boolean,
@@ -235,14 +239,16 @@ const ScreenSelector: ComponentType<{
Screenplay simulation
-
+ {anyVariantsExist && (
+
+ )}
diff --git a/assets/src/util/admin.tsx b/assets/src/util/admin.tsx
index 8412e3b60..22290df72 100644
--- a/assets/src/util/admin.tsx
+++ b/assets/src/util/admin.tsx
@@ -38,7 +38,7 @@ export const SCREEN_APPS: { [key in AppId]: AppInfo } = {
bus_eink_v2: { name: "Bus E-ink", hasAudio: true, variants: [] },
bus_shelter_v2: { name: "Bus Shelter", hasAudio: true, variants: [] },
busway_v2: { name: "Sectional", hasAudio: true, variants: [] },
- dup_v2: { name: "DUP", hasAudio: false, variants: ["new_departures"] },
+ dup_v2: { name: "DUP", hasAudio: false, variants: [] },
gl_eink_v2: { name: "GL E-ink", hasAudio: true, variants: [] },
pre_fare_v2: { name: "Pre-Fare", hasAudio: true, variants: [] },
};
diff --git a/config/config.exs b/config/config.exs
index 52d8da787..57b1aa4bc 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -471,16 +471,7 @@ config :screens,
"Needham Heights" => "Needham Hts",
"Houghs Neck via McGrath & Germantown" => "Houghs Neck via McGth & Gtwn",
"Houghs Neck via Germantown" => "Houghs Neck via Germntwn"
- },
- dup_headway_branch_stations: ["place-kencl", "place-jfk", "place-coecl"],
- dup_headway_branch_terminals: [
- "Boston College",
- "Cleveland Circle",
- "Riverside",
- "Heath Street",
- "Ashmont",
- "Braintree"
- ]
+ }
config :screens, :screens_by_alert,
cache_module: Screens.ScreensByAlert.GenServer,
diff --git a/lib/screens/alerts/alert.ex b/lib/screens/alerts/alert.ex
index 5b68b8eee..60023c67d 100644
--- a/lib/screens/alerts/alert.ex
+++ b/lib/screens/alerts/alert.ex
@@ -172,22 +172,6 @@ defmodule Screens.Alerts.Alert do
end
end
- @doc """
- Convenience for cases when it's safe to treat an API alert data outage
- as if there simply aren't any alerts for the given parameters.
-
- If the query fails for any reason, an empty list is returned.
-
- Currently used for DUPs
- """
- @spec fetch_or_empty_list(keyword()) :: list(t())
- def fetch_or_empty_list(opts \\ []) do
- case fetch(opts) do
- {:ok, alerts} -> alerts
- :error -> []
- end
- end
-
@doc """
Used by V2 e-ink and bus shelter alerts
diff --git a/lib/screens/headways.ex b/lib/screens/headways.ex
index e11a6c3f7..325cfda61 100644
--- a/lib/screens/headways.ex
+++ b/lib/screens/headways.ex
@@ -4,7 +4,6 @@ defmodule Screens.Headways do
when no live departures are available ("Trains every X-Y minutes").
"""
- alias Screens.Routes.Route
alias Screens.SignsUiConfig.Cache, as: SignsUi
alias Screens.Stops.Stop
alias Screens.Util
@@ -38,98 +37,6 @@ defmodule Screens.Headways do
silver_chelsea: [7096..7097, 74_611..74_617, 74_630..74_637]
}
- # Mapping of parent station IDs and route IDs to headway keys. For brevity, omits the "place-"
- # prefix which is currently common to all parent station IDs.
- @stations %{
- blue_trunk: {
- ~w[Blue],
- ~w[wondl rbmnl bmmnl sdmnl orhte wimnl aport mvbcl aqucl state gover bomnl]
- },
- glx_medford: {~w[Green-E], ~w[mdftf balsq mgngl gilmn esomr]},
- glx_union: {~w[Green-D], ~w[unsqu]},
- green_b: {
- ~w[Green-B],
- ~w[
- bland
- buest
- bucen
- amory
- babck
- brico
- harvd
- grigg
- alsgr
- wrnst
- wascm
- sthld
- chswk
- chill
- sougr
- lake
- ]
- },
- green_c: {
- ~w[Green-C],
- ~w[smary hwsst kntst stpul cool sumav bndhl fbkst bcnwa tapst denrd engav clmnl]
- },
- green_d: {
- ~w[Green-D],
- ~w[fenwy longw bvmnl brkhl bcnfd rsmnl chhil newto newtn eliot waban woodl river]
- },
- green_e: {
- ~w[Green-E],
- ~w[prmnl symcl nuniv mfa lngmd brmnl fenwd mispk rvrwy bckhl hsmnl]
- },
- # Not all of these stations are served by all Green Line branches, but "trunk" is the most
- # granular division of the Green Line we have to work with
- green_trunk: {
- ~w[Green-B Green-C Green-D Green-E],
- ~w[lech spmnl north haecl gover pktrm boyls armnl coecl hymnl kencl]
- },
- orange_trunk: {
- ~w[Orange],
- ~w[
- ogmnl
- mlmnl
- welln
- astao
- sull
- ccmnl
- north
- haecl
- state
- dwnxg
- chncl
- tumnl
- bbsta
- masta
- rugg
- rcmnl
- jaksn
- sbmnl
- grnst
- forhl
- ]
- },
- red_trunk: {
- ~w[Red],
- ~w[alfcl davis portr harsq cntsq knncl chmnl pktrm dwnxg sstat brdwy andrw jfk]
- },
- red_ashmont: {~w[Red], ~w[shmnl fldcr smmnl asmnl]},
- red_braintree: {~w[Red], ~w[nqncy wlsta qnctr qamnl brntn]},
- # combining 743 with the other three for the common stops
- silver_seaport: {~w[741 742 743 746], ~w[conrd wtcst crtst sstat]},
- silver_chelsea: {~w[743], ~w[aport estav boxdt belsq chels]}
- }
-
- # Mapping of stops and route IDs to headway keys for the Silver Line,
- # for stops which serve more than one route.
- @sl_stops %{
- # congress_st_at_wtc 17_096
- # combining 743 with the other three for the common stops
- silver_seaport: {~w[741 742 743 746], ~w[17096]}
- }
-
@type range :: {low :: pos_integer(), high :: pos_integer()}
@doc """
@@ -145,42 +52,12 @@ defmodule Screens.Headways do
end
end
- @doc """
- Gets headway values for a stop, with a route provided to disambiguate when the stop is a parent
- station served by multiple routes.
-
- ⚠️ Included for compatibility with existing DUP departures logic. Prefer `get/2`.
- """
- @callback get_with_route(Stop.id(), Route.id()) :: range() | nil
- @callback get_with_route(Stop.id(), Route.id(), DateTime.t()) :: range() | nil
- def get_with_route(stop_id, route_id, at \\ DateTime.utc_now()) do
- case headway_key(stop_id, route_id) do
- nil -> nil
- key -> key |> SignsUi.headways() |> Map.get(period(at))
- end
- end
-
@spec headway_key(Stop.id()) :: SignsUi.headway_key() | nil
- @spec headway_key(Stop.id(), Route.id()) :: SignsUi.headway_key() | nil
for {key, ranges} <- @stops, range <- ranges, stop_id <- range do
defp headway_key(unquote(to_string(stop_id))), do: unquote(to_string(key))
- defp headway_key(unquote(to_string(stop_id)), _route_id), do: unquote(to_string(key))
- end
-
- for {key, {route_ids, stations}} <- @stations,
- route_id <- route_ids,
- station <- stations do
- defp headway_key("place-" <> unquote(station), unquote(route_id)), do: unquote(to_string(key))
- end
-
- for {key, {route_ids, stations}} <- @sl_stops,
- route_id <- route_ids,
- station <- stations do
- defp headway_key(unquote(station), unquote(route_id)), do: unquote(to_string(key))
end
defp headway_key(_stop_id), do: nil
- defp headway_key(_stop_id, _route_id), do: nil
@spec period(DateTime.t()) :: :peak | :off_peak | :saturday | :sunday
defp period(datetime) do
diff --git a/lib/screens/predictions/prediction.ex b/lib/screens/predictions/prediction.ex
index d51f512f0..8bed84fdf 100644
--- a/lib/screens/predictions/prediction.ex
+++ b/lib/screens/predictions/prediction.ex
@@ -42,6 +42,10 @@ defmodule Screens.Predictions.Prediction do
def stop_for_vehicle(%__MODULE__{vehicle: %Vehicle{stop_id: stop_id}}), do: stop_id
def stop_for_vehicle(_), do: nil
+ @spec time(t()) :: DateTime.t() | nil
+ def time(%__MODULE__{arrival_time: arrival, departure_time: departure}),
+ do: arrival || departure
+
def vehicle_status(%__MODULE__{vehicle: %Vehicle{current_status: current_status}}),
do: current_status
diff --git a/lib/screens/schedules/schedule.ex b/lib/screens/schedules/schedule.ex
index 983f2b780..383a94d81 100644
--- a/lib/screens/schedules/schedule.ex
+++ b/lib/screens/schedules/schedule.ex
@@ -45,5 +45,13 @@ defmodule Screens.Schedules.Schedule do
end
end
+ @spec headsign(t()) :: String.t()
+ def headsign(%__MODULE__{stop_headsign: headsign}) when not is_nil(headsign), do: headsign
+ def headsign(%__MODULE__{trip: %Trip{headsign: headsign}}), do: headsign
+
+ @spec time(t()) :: DateTime.t() | nil
+ def time(%__MODULE__{arrival_time: arrival, departure_time: departure}),
+ do: arrival || departure
+
defp current_service_date(now \\ DateTime.utc_now()), do: Util.service_date(now)
end
diff --git a/lib/screens/v2/candidate_generator/dup.ex b/lib/screens/v2/candidate_generator/dup.ex
index 7894788bf..b71ae191d 100644
--- a/lib/screens/v2/candidate_generator/dup.ex
+++ b/lib/screens/v2/candidate_generator/dup.ex
@@ -2,13 +2,21 @@ defmodule Screens.V2.CandidateGenerator.Dup do
@moduledoc false
alias Screens.V2.CandidateGenerator
- alias Screens.V2.CandidateGenerator.Dup.Alerts, as: AlertsInstances
- alias Screens.V2.CandidateGenerator.Dup.Departures, as: DeparturesInstances
+ alias Screens.V2.CandidateGenerator.Dup.Alerts, as: AlertsGenerator
+ alias Screens.V2.CandidateGenerator.Dup.Departures, as: DeparturesGenerator
alias Screens.V2.CandidateGenerator.Widgets
alias Screens.V2.Template.Builder
@behaviour CandidateGenerator
+ @instance_generators [
+ &AlertsGenerator.alert_instances/2,
+ &DeparturesGenerator.instances/2,
+ &Widgets.EmergencyTakeover.emergency_takeover_instances/2,
+ &Widgets.Evergreen.evergreen_content_instances/2,
+ &Widgets.Header.instances/2
+ ]
+
@impl CandidateGenerator
def screen_template(_screen) do
{:screen,
@@ -69,26 +77,8 @@ defmodule Screens.V2.CandidateGenerator.Dup do
end
@impl CandidateGenerator
- def candidate_instances(
- config,
- now \\ DateTime.utc_now(),
- header_instances_fn \\ &Widgets.Header.instances/2,
- evergreen_content_instances_fn \\ &Widgets.Evergreen.evergreen_content_instances/2,
- departures_instances_fn \\ &DeparturesInstances.departures_instances/2,
- alerts_instances_fn \\ &AlertsInstances.alert_instances/2,
- emergency_takeover_instances_fn \\ &Widgets.EmergencyTakeover.emergency_takeover_instances/2
- ) do
- CandidateGenerator.async_stream(
- [
- fn -> header_instances_fn.(config, now) end,
- fn -> alerts_instances_fn.(config, now) end,
- fn -> departures_instances_fn.(config, now) end,
- fn -> evergreen_content_instances_fn.(config, now) end,
- fn -> emergency_takeover_instances_fn.(config, now) end
- ],
- & &1.(),
- timeout: 20_000
- )
+ def candidate_instances(config, now \\ DateTime.utc_now()) do
+ CandidateGenerator.async_stream(@instance_generators, & &1.(config, now), timeout: 20_000)
end
@impl CandidateGenerator
diff --git a/lib/screens/v2/candidate_generator/dup/departures.ex b/lib/screens/v2/candidate_generator/dup/departures.ex
index f109aa001..6609aa9f5 100644
--- a/lib/screens/v2/candidate_generator/dup/departures.ex
+++ b/lib/screens/v2/candidate_generator/dup/departures.ex
@@ -1,793 +1,441 @@
defmodule Screens.V2.CandidateGenerator.Dup.Departures do
@moduledoc false
- require Logger
+ import Screens.Inject
- alias Screens.Alerts.{Alert, InformedEntity}
- alias Screens.Report
+ alias Screens.Lines.Line
alias Screens.Routes.Route
alias Screens.Schedules.Schedule
- alias Screens.Stops.Stop
- alias Screens.Util
- alias Screens.V2.CandidateGenerator.Widgets
+ alias Screens.Trips.Trip
+
alias Screens.V2.Departure
+ alias Screens.V2.RDS
+ alias Screens.V2.RDS.{Countdowns, FirstTrip, Headways, NoService, ServiceEnded}
+
alias Screens.V2.WidgetInstance.Departures, as: DeparturesWidget
- alias Screens.V2.WidgetInstance.{DeparturesNoData, OvernightDepartures}
- alias Screens.Vehicles.Vehicle
- alias ScreensConfig.{Departures, Screen}
- alias ScreensConfig.Departures.{Header, Layout, Query, Section}
- alias ScreensConfig.Departures.Query.Params
- alias ScreensConfig.Screen.Dup
alias Screens.V2.WidgetInstance.Departures.{
+ HeadwayRow,
HeadwaySection,
NoDataSection,
NormalSection,
+ NoServiceSection,
OvernightSection
}
- import Screens.Inject
+ alias Screens.V2.WidgetInstance.{DeparturesNoData, DeparturesNoService, OvernightDepartures}
+
+ alias ScreensConfig.Departures
+ alias ScreensConfig.Departures.{Header, Layout, Section}
+ alias ScreensConfig.Screen
+ alias ScreensConfig.Screen.Dup
+
+ @type widget :: DeparturesNoData.t() | DeparturesWidget.t() | OvernightDepartures.t()
+ @rds injected(RDS)
+
+ @max_departures_per_rotation 4
- @headways injected(Screens.Headways)
+ @primary_slot_names [
+ :main_content_zero,
+ :main_content_one,
+ :main_content_reduced_zero,
+ :main_content_reduced_one
+ ]
+ @secondary_slot_names [:main_content_two, :main_content_reduced_two]
- def departures_instances(
+ @spec instances(Screen.t(), DateTime.t()) :: [widget()]
+ def instances(
%Screen{
app_params: %Dup{
- primary_departures: %Departures{sections: primary_sections},
- secondary_departures: %Departures{sections: secondary_sections}
+ primary_departures: primary_departures,
+ secondary_departures: secondary_departures
}
} = config,
- now,
- fetch_departures_fn \\ fn params, opts ->
- Departure.fetch(params, Keyword.put(opts, :include_scheduled_cancelled?, true))
- end,
- fetch_alerts_fn \\ &Alert.fetch_or_empty_list/1,
- fetch_schedules_fn \\ &Screens.Schedules.Schedule.fetch/2,
- fetch_routes_fn \\ &Screens.Routes.Route.fetch/1,
- fetch_vehicles_fn \\ &Screens.Vehicles.Vehicle.by_route_and_direction/2
+ now
) do
- [primary_instance, secondary_instance] =
- Enum.map([primary_sections, secondary_sections], fn sections ->
- sections
- |> get_sections_data(fetch_departures_fn, fetch_alerts_fn, fetch_routes_fn, now)
- |> sections_data_to_instance_fn(config, now, fetch_schedules_fn, fetch_vehicles_fn)
- end)
-
- # When the secondary instance would not be displaying anything useful, make it a copy of the
- # primary instance.
- secondary_instance =
- if secondary_sections == [] or no_departures?(secondary_instance),
- do: primary_instance,
- else: secondary_instance
-
- primary_instances =
- Enum.map(
- [
- :main_content_zero,
- :main_content_one,
- :main_content_reduced_zero,
- :main_content_reduced_one
- ],
- &struct!(primary_instance, slot_names: [&1])
- )
-
- secondary_instances =
- Enum.map(
- [:main_content_two, :main_content_reduced_two],
- &struct!(secondary_instance, slot_names: [&1])
- )
-
- instances = primary_instances ++ secondary_instances
+ primary_rds_sections = @rds.get(primary_departures, now)
- cond do
- # If every rotation is showing OvernightDepartures, we don't need to render any route pills.
- Enum.all?(instances, &is_struct(&1, OvernightDepartures)) ->
- Enum.map(instances, &%OvernightDepartures{&1 | routes: []})
-
- # If every rotation consists entirely of NoDataSections, replace all with DeparturesNoData.
- Enum.all?(instances, &no_departures?/1) ->
- Enum.map(instances, fn %DeparturesWidget{screen: screen, slot_names: [slot_name]} ->
- %DeparturesNoData{screen: screen, slot_name: slot_name}
- end)
+ secondary_rds_sections = @rds.get(secondary_departures, now)
- true ->
- instances
- end
- end
+ primary_departure_sections =
+ create_departure_sections(primary_rds_sections, primary_departures)
- defp no_departures?(%DeparturesWidget{sections: sections}),
- do: Enum.all?(sections, &is_struct(&1, NoDataSection))
+ secondary_departure_sections =
+ if secondary_rds_sections == [] or Enum.all?(secondary_rds_sections, &(&1 == {:ok, []})) do
+ primary_departure_sections
+ else
+ create_departure_sections(secondary_rds_sections, secondary_departures)
+ end
- defp no_departures?(_widget_instance), do: false
+ all_sections_no_data =
+ Enum.all?(
+ primary_departure_sections ++ secondary_departure_sections,
+ &is_struct(&1, NoDataSection)
+ )
- @typep section_data ::
- %{type: :no_data_section, route: Route.t()}
- | %{
- departures: [Departure.t()],
- alert_informed_entities: [InformedEntity.t()],
- stop_ids: [Stop.id()],
- routes: [Route.t()],
- params: Params.t()
- }
+ all_sections_service_ended =
+ Enum.all?(
+ primary_departure_sections ++ secondary_departure_sections,
+ &is_struct(&1, OvernightSection)
+ )
- @spec sections_data_to_instance_fn(
- [section_data()],
- Screen.t(),
- DateTime.t(),
- Schedule.fetch_with_date(),
- Vehicle.by_route_and_direction()
- ) :: DeparturesWidget.t() | OvernightDepartures.t()
- defp sections_data_to_instance_fn(
- sections_data,
- config,
- now,
- fetch_schedules_fn,
- fetch_vehicles_fn
- ) do
- is_only_section = match?([_], sections_data)
+ primary_instances =
+ build_instances(
+ @primary_slot_names,
+ primary_departure_sections,
+ all_sections_no_data,
+ all_sections_service_ended,
+ config,
+ now
+ )
- sections =
- Enum.map(
- sections_data,
- &get_section_instance(&1, is_only_section, now, fetch_schedules_fn, fetch_vehicles_fn)
+ secondary_instances =
+ build_instances(
+ @secondary_slot_names,
+ secondary_departure_sections,
+ all_sections_no_data,
+ all_sections_service_ended,
+ config,
+ now
)
- # NB: No slot names provided here (defaults to `[]`) as they will be filled in depending on
- # whether the widget is placed in the primary or secondary slots.
- if Enum.any?(sections) and Enum.all?(sections, &is_struct(&1, OvernightSection)) do
- route_pills = get_route_pills_for_rotation(sections)
- %OvernightDepartures{screen: config, routes: route_pills}
- else
- %DeparturesWidget{screen: config, sections: sections, now: now}
- end
+ primary_instances ++ secondary_instances
end
- @spec get_section_instance(
- section_data(),
- boolean(),
- DateTime.t(),
- Schedule.fetch_with_date(),
- Vehicle.by_route_and_direction()
- ) :: DeparturesWidget.section()
- defp get_section_instance(
- %{type: :no_data_section, route: route},
- _is_only_section,
- _now,
- _fetch_schedules_fn,
- _fetch_vehicles_fn
- ),
- do: %NoDataSection{route: route}
-
- defp get_section_instance(
- %{
- departures: departures,
- alert_informed_entities: alert_informed_entities,
- stop_ids: stop_ids,
- routes: routes,
- params: params
- },
- is_only_section,
- now,
- fetch_schedules_fn,
- fetch_vehicles_fn
- ) do
- routes_with_live_departures =
- departures
- |> Enum.map(&{Departure.route(&1).id, Departure.direction_id(&1)})
- |> Enum.uniq()
-
- max_visible_departures = if is_only_section, do: 4, else: 2
- # Check if there is any room for overnight rows before running the logic.
+ defp create_departure_sections(rds_sections, %Departures{sections: departure_sections}) do
+ section_count = length(rds_sections)
- route_and_direction_schedules =
- get_route_direction_schedules(params, routes, now, fetch_schedules_fn)
-
- {section_contains_active_route, overnight_schedules_for_section} =
- if length(departures) >= max_visible_departures do
- {false, []}
- else
- get_overnight_schedules_for_section(
- route_and_direction_schedules,
- routes_with_live_departures,
- params,
- routes,
- alert_informed_entities,
- now,
- fetch_vehicles_fn
- )
- end
+ Enum.zip(rds_sections, departure_sections)
+ |> Enum.map(fn {rds_section, %Section{bidirectional: bidirectional}} ->
+ map_to_departure_section(rds_section, bidirectional, section_count)
+ end)
+ end
- in_overnight_period =
- !section_contains_active_route and
- overnight_schedules_for_section != [] and
- overnight_time_period?(route_and_direction_schedules, now)
+ @spec map_to_departure_section(RDS.section_t(), boolean(), number()) ::
+ DeparturesWidget.section()
- representative_route = hd(routes)
+ defp map_to_departure_section(:error, _, _), do: %NoDataSection{}
- alert_headsign =
- headsign_for_alert(alert_informed_entities, stop_ids)
+ defp map_to_departure_section({:ok, []}, _, _), do: %NoDataSection{}
- headway_time_range = headway_time_range(stop_ids, routes, now)
+ defp map_to_departure_section({:ok, rds_list}, bidirectional, section_count) do
+ num_departures_per_section = div(@max_departures_per_rotation, section_count)
cond do
- # Normal departures mode
- departures != [] ->
- # Add overnight departures to the end.
- # This allows overnight departures to appear as we start to run out of predictions to show.
- visible_departures =
- Enum.take(departures ++ overnight_schedules_for_section, max_visible_departures)
-
- # DUPs don't support Layout or Header for now
- %NormalSection{rows: visible_departures, layout: %Layout{}, header: %Header{}}
-
- # If we have no departures, go through the other options
-
- # In cases of temporary terminals, we want to show headways with directional headsigns
- alert_headsign && headway_time_range && !in_overnight_period ->
- %HeadwaySection{
- route: get_section_route_from_entities(stop_ids, alert_informed_entities),
- time_range: headway_time_range,
- headsign:
- if is_only_section do
- alert_headsign
- else
- direction_name(representative_route, params.direction_id)
- end
+ no_service?(rds_list) ->
+ %NoServiceSection{
+ routes:
+ rds_list
+ |> Enum.flat_map(fn %RDS{state: %NoService{routes: routes}} -> routes end)
+ |> Enum.uniq()
}
- # If we're not showing a headway for alerts, and all routes in the section are disabled, so no departures are expected.
- # In this case, the alerts widget will display info on the closure, so we return an empty section.
- section_routes_disabled?(routes, params.direction_id, alert_informed_entities) ->
- %NormalSection{rows: departures, layout: %Layout{}, header: %Header{}}
-
- # If no departures, no alerts, and not in the overnight period, we show Headways
- headway_time_range &&
- (alert_informed_entities == [] && !in_overnight_period) ->
- %HeadwaySection{
- route: representative_route.id,
- time_range: headway_time_range,
- headsign: direction_name(representative_route, params.direction_id)
+ service_ended?(rds_list) ->
+ %OvernightSection{
+ routes:
+ rds_list
+ |> Enum.map(fn %RDS{state: %ServiceEnded{last_schedule: %Schedule{route: route}}} ->
+ route
+ end)
+ |> Enum.uniq()
}
- # No remaining departures or active routes, but we do have routes with overnight schedules
- # Show a takeover Overnight section for the given route types
- in_overnight_period ->
- %OvernightSection{routes: routes}
+ headways?(rds_list) ->
+ create_headway_section(rds_list)
- # No departures to show and no headway mode
true ->
- %NoDataSection{route: representative_route}
+ %NormalSection{
+ rows:
+ create_and_sort_rows(rds_list)
+ |> maybe_make_bidirectional(bidirectional)
+ |> Enum.take(num_departures_per_section),
+ layout: %Layout{},
+ header: %Header{}
+ }
end
end
- # Determines if the active alerts for a section apply to all routes that are enabled in the section
- @spec section_routes_disabled?([Route.t()], 0 | 1 | :both, [InformedEntity.t()]) :: boolean()
- defp section_routes_disabled?(routes, direction_id, alert_informed_entities) do
- case alert_informed_entities do
- [] ->
- false
-
- _ ->
- # Normalize direction_id. Typically `nil` in Informed Entity represents both directions
- direction_id =
- case direction_id do
- :both -> nil
- _ -> direction_id
- end
-
- # For each route, verify if there is an associated Informed Entity
- routes
- |> Enum.map(& &1.id)
- |> Enum.all?(fn route_id ->
- Enum.any?(alert_informed_entities, fn entity ->
- InformedEntity.present_alert_for_route?(entity, route_id, direction_id)
- end)
- end)
- end
+ defp no_service?(rds_list) do
+ Enum.all?(rds_list, &is_struct(&1.state, NoService))
end
- @spec get_sections_data(
- [Section.t()],
- Departure.fetch(),
- Alert.fetch(),
- Route.fetch(),
- now :: DateTime.t()
- ) :: [section_data()]
- defp get_sections_data(
- sections,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_routes_fn,
- now
- ) do
- sections
- |> Task.async_stream(
- &get_section_data(&1, fetch_departures_fn, fetch_alerts_fn, fetch_routes_fn, now),
- on_timeout: :kill_task
- )
- |> Enum.map(fn
- {:ok, data} ->
- data
-
- {:exit, reason} ->
- Logger.error("get_section_data.exit reason=#{reason}")
- raise "Failed to get section data"
- end)
+ defp service_ended?(rds_list) do
+ Enum.all?(rds_list, &is_struct(&1.state, ServiceEnded))
end
- defp get_section_data(
- %Section{query: %Query{params: %Params{stop_ids: stop_ids} = params}} = section,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_routes_fn,
- now
- ) do
- routes = get_routes_serving_section(params, fetch_routes_fn)
- # DUP sections will always show no more than one mode.
- # For subway, each route will have its own section.
- # If the stop is served by two different subway/light rail routes, route_ids must be populated for each section
- # Otherwise, we only need the first route in the list of routes serving the stop.
- primary_route_for_section = List.first(routes)
-
- disabled_modes = Screens.Config.Cache.disabled_modes()
-
- # If we know the predictions are unreliable, don't even bother fetching them.
- if is_nil(primary_route_for_section) or primary_route_for_section.type in disabled_modes do
- %{type: :no_data_section, route: primary_route_for_section}
- else
- section_departures =
- case Widgets.Departures.fetch_section_departures(
- section,
- disabled_modes,
- fetch_departures_fn
- ) do
- {:ok, departures} -> departures
- :error -> []
- end
-
- alert_informed_entities = get_section_entities(params, fetch_alerts_fn, now)
+ defp headways?(rds_list) do
+ Enum.all?(rds_list, &is_struct(&1.state, Headways))
+ end
- %{
- departures: section_departures,
- alert_informed_entities: alert_informed_entities,
- stop_ids: stop_ids,
- routes: routes,
- params: params
- }
- end
+ @spec create_headway_section([RDS.t()]) :: HeadwaySection.t()
+
+ # Bidirectional -> Use no headsign for the trains message
+ defp create_headway_section([
+ %RDS{state: %{route_id: route_id, direction_id: direction_id_one, range: range}}
+ | [%RDS{state: %{route_id: route_id, direction_id: direction_id_two, range: range}}]
+ ])
+ when direction_id_one != direction_id_two do
+ %HeadwaySection{
+ route: route_id,
+ time_range: range,
+ headsign: nil
+ }
end
- defp get_section_route_from_entities(
- ["place-" <> _ = stop_id],
- informed_entities
+ # Use the headsign if the destinations have the same headsign,
+ # use the direction name if they have the same direction name,
+ # otherwise default to no headsign
+ defp create_headway_section(
+ [
+ %RDS{
+ headsign: headsign,
+ line: %Line{id: first_line_id},
+ state: %Headways{
+ route_id: route_id,
+ direction_name: direction_name,
+ direction_id: direction_id,
+ range: range
+ }
+ }
+ | _
+ ] = destinations
) do
- Enum.find_value(informed_entities, "", fn
- %InformedEntity{route: route, stop: %{id: ^stop_id}} -> route
- _ -> nil
- end)
- end
+ %HeadwaySection{
+ route: route_id,
+ time_range: range,
+ headsign:
+ cond do
+ Enum.all?(destinations, fn %RDS{headsign: other_headsign} ->
+ headsign == other_headsign
+ end) ->
+ headsign
+
+ Enum.all?(
+ destinations,
+ fn %RDS{
+ line: %Line{id: other_line_id},
+ state: %Headways{direction_id: other_direction_id}
+ } ->
+ first_line_id == other_line_id and direction_id == other_direction_id
+ end
+ ) ->
+ direction_name
- defp get_section_route_from_entities(_, _), do: nil
+ true ->
+ nil
+ end
+ }
+ end
- defp get_section_entities(
- %Params{
- stop_ids: stop_ids,
- route_ids: route_ids,
- direction_id: direction_id
- },
- fetch_alerts_fn,
- now
+ defp build_instances(
+ slot_names,
+ _departure_sections,
+ true = _all_section_no_data,
+ _all_section_service_ended,
+ config,
+ _now
) do
- alert_fetch_params = [
- direction_id: direction_id,
- route_ids: route_ids,
- stop_ids: stop_ids,
- route_types: [:light_rail, :subway]
- ]
-
- # This section gets alert entities, which are used to decide whether we should be in headway mode or overnight mode
- # Also used to check if no departures are expected for a section because of closures to all routes/directions
- alert_fetch_params
- |> fetch_alerts_fn.()
- |> Enum.filter(fn
- # Show a headway message only during shuttles and suspensions at temporary terminals.
- # https://www.notion.so/mbta-downtown-crossing/Departures-Widget-Specification-20da46cd70a44192a568e49ea47e09ac?pvs=4#e43086abaadd465ea8072502d6980d8d
- %Alert{effect: effect} = alert when effect in [:suspension, :shuttle] ->
- Alert.happening_now?(alert, now)
-
- _ ->
- false
- end)
- # Condense all alerts into just a list of informed entities
- # This will help us decide whether a headway in one direction is still useful
- # if there are two alerts that could be in different directions.
- |> Enum.reduce([], fn alert, acc -> acc ++ alert.informed_entities end)
- |> Enum.uniq()
+ Enum.map(slot_names, &%DeparturesNoData{screen: config, slot_name: &1})
end
- defp headsign_for_alert([], _), do: nil
-
- defp headsign_for_alert(informed_entities, stop_ids) do
- # Use all informed_entities from relevant alerts to decide whether there's
- # a reason to go into headway mode for an alert
- # For example, a NB suspension and SB shuttle from Aquarium shouldn't use headway mode
- # but if all the WB branches are shuttling at Kenmore, there should be a headway for the alert
- interpreted_alert = interpret_entities(informed_entities, stop_ids)
-
- if temporary_terminal?(interpreted_alert) and
- not (branch_station?(stop_ids) and branch_alert?(interpreted_alert)) do
- interpreted_alert.headsign
- else
- nil
- end
+ defp build_instances(
+ slot_names,
+ _departure_sections,
+ _all_section_no_data,
+ true = _all_section_service_ended,
+ config,
+ _now
+ ) do
+ Enum.map(slot_names, &%OvernightDepartures{screen: config, slot_names: [&1]})
end
- # NB: There aren't currently any DUPs at permanent terminals, so we assume all
- # terminals are temporary. In the future, we'll need to check that the boundary
- # isn't a normal terminal.
- defp temporary_terminal?(%{region: :boundary}), do: true
- defp temporary_terminal?(_), do: false
+ defp build_instances(
+ slot_names,
+ departure_sections,
+ _all_section_no_data,
+ _all_section_service_ended,
+ config,
+ now
+ ) do
+ cond do
+ Enum.all?(departure_sections, &is_struct(&1, NoServiceSection)) ->
+ Enum.map(
+ slot_names,
+ &%DeparturesNoService{
+ screen: config,
+ slot_name: &1,
+ routes:
+ departure_sections
+ |> Enum.flat_map(fn %NoServiceSection{routes: routes} -> routes end)
+ |> Enum.map(fn route -> Route.icon(route) end)
+ |> Enum.uniq()
+ }
+ )
- defp branch_station?(stop_ids) do
- branch_stations = Application.get_env(:screens, :dup_headway_branch_stations)
+ Enum.all?(departure_sections, &is_struct(&1, OvernightSection)) ->
+ Enum.map(
+ slot_names,
+ &%OvernightDepartures{
+ screen: config,
+ slot_names: [&1],
+ routes:
+ departure_sections
+ |> Enum.flat_map(fn %OvernightSection{routes: routes} -> routes end)
+ |> Enum.map(fn route -> Route.icon(route) end)
+ |> Enum.uniq()
+ }
+ )
- case stop_ids do
- [parent_station_id] -> parent_station_id in MapSet.new(branch_stations)
- _ -> false
+ true ->
+ Enum.map(
+ slot_names,
+ §ions_to_departure_widget(&1, departure_sections, config, now)
+ )
end
end
- defp branch_alert?(%{headsign: headsign}) do
- branch_terminals = Application.get_env(:screens, :dup_headway_branch_terminals)
- headsign in MapSet.new(branch_terminals)
- end
-
- defp interpret_entities(entities, parent_stop_ids) do
- informed_stop_ids = Enum.into(entities, MapSet.new(), & &1.stop.id)
- parent_stop_id = List.first(parent_stop_ids)
-
- {region, headsign} =
- :screens
- |> Application.get_env(:dup_alert_headsign_matchers)
- |> Map.get(parent_stop_id, [])
- |> Enum.find_value({:inside, nil}, fn
- %{
- informed: informed,
- not_informed: not_informed,
- headway_headsign: headsign
- } ->
- if alert_region_match?(
- Util.to_set(informed),
- Util.to_set(not_informed),
- informed_stop_ids
- ),
- do: {:boundary, headsign},
- else: false
-
- _ ->
- false
- end)
-
- %{
- region: region,
- headsign: headsign
+ defp sections_to_departure_widget(slot_name, departure_sections, config, now) do
+ %DeparturesWidget{
+ screen: config,
+ sections: departure_sections,
+ slot_names: [slot_name],
+ now: now
}
end
- defp alert_region_match?(informed, not_informed, informed_stop_ids) do
- MapSet.subset?(informed, informed_stop_ids) and
- MapSet.disjoint?(not_informed, informed_stop_ids)
- end
-
- defp overnight_time_period?(route_and_direction_schedules, now) do
- {first_time, last_time} =
- route_and_direction_schedules
- |> Enum.reduce({nil, nil}, fn {_route_and_direction,
- {first_schedule_today, last_schedule_today,
- _first_schedule_tomorrow}},
- {first_scheduled_time, last_scheduled_time} ->
- first_time =
- cond do
- is_nil(first_scheduled_time) ->
- first_schedule_today.departure_time
-
- DateTime.compare(first_schedule_today.departure_time, first_scheduled_time) == :lt ->
- first_schedule_today.departure_time
-
- true ->
- first_scheduled_time
- end
-
- last_time =
- cond do
- is_nil(last_scheduled_time) ->
- last_schedule_today.departure_time
-
- DateTime.compare(last_schedule_today.departure_time, last_scheduled_time) == :gt ->
- last_schedule_today.departure_time
-
- true ->
- last_scheduled_time
- end
-
- {first_time, last_time}
+ @spec create_and_sort_rows([RDS.t()]) :: [NormalSection.row()]
+ defp create_and_sort_rows(rds_list) do
+ grouped_rds =
+ Enum.group_by(rds_list, fn
+ %RDS{state: %ServiceEnded{}} -> :service_ended
+ %RDS{state: %Headways{}} -> :headways
+ _ -> :other
end)
- DateTime.compare(now, first_time) == :gt or
- DateTime.compare(now, last_time) == :lt
+ service_ended_rds = Map.get(grouped_rds, :service_ended, [])
+ headway_rds = Map.get(grouped_rds, :headways, [])
+ rds = Map.get(grouped_rds, :other, [])
+
+ sorted_departures_from_rds(rds) ++
+ headways_from_rds(rds ++ service_ended_rds, headway_rds) ++
+ sorted_departures_from_rds(service_ended_rds, true)
end
- # Return 'overnight' Departures from `get_overnight_schedules_for_section` to potentially show
- # if one of the following is true for a given route_id/direction_id combo:
- # 1. Service for the route is done for the day, so we may display the first departure of tomorrow.
- # 2. Service for the route has not started for the day, so we may display the first departure of today.
- # 3. Service for the route is done for the day and not scheduled tomorrow
- # (possible for CR/buses/routes with interruptions tomorrow), so return a Departure
- # with nil departure_time and arrival_time to be handled by the serializer.
- #
- # Returns a tuple where:
- # - First value is a boolean to track if there are any remaining scheduled trips for the section.
- # - Second value is a list of Departure Schedules to potentially show on screen given enough space.
- defp get_overnight_schedules_for_section(
- route_and_direction_schedules,
- routes_with_live_departures,
- stop_ids,
- routes_serving_section,
- alert_informed_entities,
- now,
- fetch_vehicles_fn
- )
-
- # No predictions AND no active alerts for the section
- defp get_overnight_schedules_for_section(
- route_and_direction_schedules,
- routes_with_live_departures,
- _params,
- _routes,
- [],
- now,
- _
- ) do
- # Get schedules for each route_id in config
- overnight_schedules =
- route_and_direction_schedules
- |> Enum.reject(fn {route_and_direction, _schedule} ->
- route_and_direction in routes_with_live_departures
- end)
- |> Enum.map(fn {{route_id, direction_id},
- {first_schedule_today, last_schedule_today, first_schedule_tomorrow}} ->
- get_overnight_departure_for_route(
- first_schedule_today,
- last_schedule_today,
- first_schedule_tomorrow,
- route_id,
- direction_id,
- now
- )
- end)
+ @spec maybe_make_bidirectional([Departure.t()], boolean()) :: [Departure.t()]
+ defp maybe_make_bidirectional([], _), do: []
+ defp maybe_make_bidirectional(departures, false), do: departures
- # We flag if any routes have service ongoing so the section doesn't show as fully overnight
- is_sections_service_active = Enum.any?(overnight_schedules, &(&1 == :route_service_ongoing))
+ defp maybe_make_bidirectional([first | rest], true) do
+ first_direction = departure_direction_id(first)
- {is_sections_service_active,
- overnight_schedules
- # Routes not in overnight mode will be nil. Can ignore those.
- |> Enum.reject(fn elem -> is_nil(elem) or elem == :route_service_ongoing end)
- |> Enum.sort_by(fn %Departure{schedule: %Schedule{departure_time: dt}} -> dt end)}
- end
+ opposite? =
+ Enum.find(rest, Enum.at(rest, 0), &(departure_direction_id(&1) == 1 - first_direction))
- defp get_overnight_schedules_for_section(
- route_and_direction_schedules,
- routes_with_live_departures,
- params,
- [%{type: type} | _] = routes,
- alert_informed_entities,
- now,
- fetch_vehicles_fn
- )
- when type in [:subway, :light_rail] do
- informed_route = get_section_route_from_entities(params.stop_ids, alert_informed_entities)
- # If there are no vehicles operating on the route, assume we are overnight.
- if not is_nil(informed_route) and fetch_vehicles_fn.(informed_route, nil) == [] do
- get_overnight_schedules_for_section(
- route_and_direction_schedules,
- routes_with_live_departures,
- params,
- routes,
- [],
- now,
- fetch_vehicles_fn
- )
- else
- {false, []}
- end
+ Enum.reject([first, opposite?], &is_nil/1)
end
- defp get_overnight_schedules_for_section(_, _, _, _, _, _, _), do: {false, []}
+ @spec sorted_departures_from_rds([RDS.t()], boolean()) :: [Departure.t()]
+ defp sorted_departures_from_rds(rds, reverse \\ false) do
+ sort_order =
+ if reverse do
+ :desc
+ else
+ :asc
+ end
- # Verifies we are meeting the timeframe conditions for overnight mode and generates the departure widget
- defp get_overnight_departure_for_route(
- first_schedule_today,
- last_schedule_today,
- _first_schedule_tomorrow,
- route_id,
- direction_id,
- _now
- )
- when is_nil(first_schedule_today) or is_nil(last_schedule_today) do
- Report.warning("dup_overnight_no_first_last_schedule",
- route_id: route_id,
- direction_id: direction_id
+ rds
+ |> Enum.flat_map(&departure_rows_from_state(&1))
+ |> Enum.sort_by(
+ &departure_time(&1),
+ {
+ sort_order,
+ DateTime
+ }
)
-
- nil
end
- defp get_overnight_departure_for_route(
- first_schedule_today,
- last_schedule_today,
- nil,
- _route_id,
- _direction_id,
- now
+ @spec headways_from_rds([RDS.t()], [RDS.t()]) :: [HeadwayRow.t()]
+ defp headways_from_rds(
+ rds,
+ headway_rds
) do
- # If now is after today's last schedule and there are no schedules tomorrow,
- # we still want a departure row without a time (will show a moon icon)
- if DateTime.compare(now, last_schedule_today.departure_time) == :gt or
- DateTime.compare(now, first_schedule_today.departure_time) == :lt do
- # nil/nil acts as a flag for the serializer to produce an `overnight` departure time
- %Departure{
- schedule: %{last_schedule_today | departure_time: nil, arrival_time: nil}
- }
- else
- # Return an atom so we can track that there is still a departure for this route during today's service.
- :route_service_ongoing
- end
+ # If there are other similar line/direction_id destinations that are in one of the other states,
+ # disregard the headway for that particular destination
+ lines_in_other_states =
+ rds
+ |> Enum.flat_map(&extract_line_direction_pairs/1)
+ |> MapSet.new()
+
+ headway_rds
+ |> Enum.reject(fn %RDS{
+ line: %Line{id: line_id},
+ state: %Headways{direction_id: direction_id}
+ } ->
+ MapSet.member?(lines_in_other_states, {line_id, direction_id})
+ end)
+ |> Enum.group_by(fn %RDS{
+ line: line,
+ state: %Headways{direction_id: direction_id}
+ } ->
+ {line, direction_id}
+ end)
+ |> Enum.flat_map(fn {{line, direction_id}, rds_list} ->
+ %RDS{headsign: headsign, state: %Headways{direction_name: direction_name, range: range}} =
+ hd(rds_list)
+
+ # If there are multiple headways with the same line but different headsigns,
+ # combine them and use the direction name
+ displayed_headsign = if length(rds_list) == 1, do: headsign, else: direction_name
+
+ [
+ %HeadwayRow{
+ line: line,
+ direction_id: direction_id,
+ range: range,
+ headsign: displayed_headsign
+ }
+ ]
+ end)
end
- # If now is before any of today's schedules or after any of tomorrow's (should never happen but just in case)
- # we do not display overnight mode.
- defp get_overnight_departure_for_route(
- first_schedule_today,
- last_schedule_today,
- first_schedule_tomorrow,
- route_id,
- direction_id,
- now
- ) do
- cond do
- DateTime.compare(now, first_schedule_tomorrow.departure_time) == :gt ->
- Report.warning("dup_overnight_after_first_schedule",
- route_id: route_id,
- direction_id: direction_id
- )
-
- nil
+ @spec departure_rows_from_state(RDS.t()) ::
+ [Departure.t()] | [NormalSection.special_trip()]
+ defp departure_rows_from_state(%RDS{state: %Countdowns{departures: departures}}), do: departures
- # Before 4am and between the `departure_time` for today's last schedule and tomorrow's first schedule
- DateTime.compare(now, last_schedule_today.departure_time) == :gt and
- DateTime.compare(now, first_schedule_tomorrow.departure_time) == :lt ->
- %Departure{schedule: first_schedule_tomorrow}
-
- # After 4am but before the first scheduled trip of the day.
- not is_nil(first_schedule_today) and
- DateTime.compare(now, first_schedule_today.departure_time) == :lt ->
- %Departure{schedule: first_schedule_today}
-
- # Before 4am and before the last scheduled trip of the day.
- # Return an atom so we can track that there is still a departure for this route during today's service.
- DateTime.compare(now, last_schedule_today.departure_time) == :lt ->
- :route_service_ongoing
-
- true ->
- nil
- end
+ defp departure_rows_from_state(%RDS{state: %FirstTrip{first_schedule: first_schedule}}) do
+ [{first_schedule, :first_trip}]
end
- defp get_today_tomorrow_schedules(
- fetch_params,
- fetch_schedules_fn,
- now,
- route_ids_serving_section
- ) do
- today =
- case fetch_schedules_fn.(fetch_params, now) do
- {:ok, schedules} when schedules != [] ->
- Enum.filter(schedules, &(&1.route.id in route_ids_serving_section))
-
- # fetch error or empty schedules
- _ ->
- []
- end
-
- tomorrow =
- case fetch_schedules_fn.(fetch_params, now |> Util.service_date() |> Date.add(1)) do
- {:ok, schedules} when schedules != [] ->
- Enum.filter(schedules, &(&1.route.id in route_ids_serving_section))
-
- # fetch error or empty schedules
- _ ->
- []
- end
-
- {today, tomorrow}
+ defp departure_rows_from_state(%RDS{state: %ServiceEnded{last_schedule: last_schedule}}) do
+ [{last_schedule, :service_ended}]
end
- defp get_route_direction_schedules(params, routes, now, fetch_schedules_fn) do
- {today_schedules, tomorrow_schedules} =
- get_today_tomorrow_schedules(
- params |> Map.from_struct() |> Map.put(:sort, "departure_time"),
- fetch_schedules_fn,
- now,
- Enum.map(routes, & &1.id)
- )
+ defp departure_rows_from_state(%RDS{state: %NoService{}}), do: []
- today_schedules
- |> Enum.map(&{&1.route.id, &1.direction_id})
- |> Enum.uniq()
- |> Enum.map(fn {route_id, direction_id} ->
- # This variable will be used when now is after the start-of-service time.
- first_schedule_today =
- Enum.find(
- today_schedules,
- &(&1.route.id == route_id and &1.direction_id == direction_id)
- )
+ @spec departure_time(Departure.t() | NormalSection.special_trip()) :: DateTime.t()
+ defp departure_time(%Departure{} = departure), do: Departure.time(departure)
+ defp departure_time({%Schedule{} = schedule, _type}), do: Schedule.time(schedule)
- last_schedule_today =
- Enum.find(
- Enum.reverse(today_schedules),
- &(&1.route.id == route_id and &1.direction_id == direction_id)
- )
+ defp departure_direction_id(%Departure{} = departure), do: Departure.direction_id(departure)
+ defp departure_direction_id({%Schedule{direction_id: direction_id}, _type}), do: direction_id
+ defp departure_direction_id(%HeadwayRow{direction_id: direction_id}), do: direction_id
- first_schedule_tomorrow =
- Enum.find(
- tomorrow_schedules,
- &(&1.route.id == route_id and &1.direction_id == direction_id)
- )
+ defp extract_line_direction_pairs(%RDS{line: %Line{id: line_id}, state: state}) do
+ case state do
+ %NoService{direction_id: nil} ->
+ []
- {{route_id, direction_id},
- {first_schedule_today, last_schedule_today, first_schedule_tomorrow}}
- end)
- end
+ %NoService{direction_id: direction_id} ->
+ [{line_id, direction_id}]
- defp get_routes_serving_section(
- %Params{route_ids: route_ids, route_type: route_type, stop_ids: stop_ids},
- fetch_routes_fn
- ) do
- params =
- %{stop_ids: stop_ids}
- |> then(&if route_ids == [], do: &1, else: Map.put(&1, :ids, route_ids))
- |> then(&if is_nil(route_type), do: &1, else: Map.put(&1, :route_types, [route_type]))
-
- case fetch_routes_fn.(params) do
- {:ok, routes} -> routes
- :error -> []
- end
- end
-
- defp get_route_pills_for_rotation(sections) do
- sections
- |> Enum.flat_map(fn %OvernightSection{routes: routes} -> Enum.map(routes, &Route.icon/1) end)
- |> Enum.uniq()
- end
+ %Countdowns{departures: [departure | _]} ->
+ [{line_id, Departure.direction_id(departure)}]
- defp headway_time_range(stop_ids, routes, now) do
- all_headways =
- for stop_id <- stop_ids, %{id: route_id} <- routes do
- @headways.get_with_route(stop_id, route_id, now)
- end
+ %FirstTrip{first_schedule: %Schedule{trip: %Trip{direction_id: direction_id}}} ->
+ [{line_id, direction_id}]
- case Enum.uniq(all_headways) do
- [{lo, hi}] -> {lo, hi}
- _ -> nil
+ %ServiceEnded{last_schedule: %Schedule{trip: %Trip{direction_id: direction_id}}} ->
+ [{line_id, direction_id}]
end
end
-
- defp direction_name(_, :both), do: nil
-
- defp direction_name(route, direction_id) do
- route
- |> Route.normalized_direction_names()
- |> Enum.at(direction_id, nil)
- end
end
diff --git a/lib/screens/v2/candidate_generator/dup_new.ex b/lib/screens/v2/candidate_generator/dup_new.ex
deleted file mode 100644
index b8564f34e..000000000
--- a/lib/screens/v2/candidate_generator/dup_new.ex
+++ /dev/null
@@ -1,30 +0,0 @@
-defmodule Screens.V2.CandidateGenerator.DupNew do
- @moduledoc false
-
- alias Screens.V2.CandidateGenerator
- alias Screens.V2.CandidateGenerator.Dup.Alerts, as: AlertsGenerator
- alias Screens.V2.CandidateGenerator.Widgets
-
- alias __MODULE__.Departures
-
- @behaviour CandidateGenerator
-
- @instance_generators [
- &Widgets.Header.instances/2,
- &Departures.instances/2,
- &Widgets.Evergreen.evergreen_content_instances/2,
- &AlertsGenerator.alert_instances/2,
- &Widgets.EmergencyTakeover.emergency_takeover_instances/2
- ]
-
- @impl CandidateGenerator
- defdelegate screen_template(screen), to: Screens.V2.CandidateGenerator.Dup
-
- @impl CandidateGenerator
- def candidate_instances(config, now \\ DateTime.utc_now()) do
- CandidateGenerator.async_stream(@instance_generators, & &1.(config, now), timeout: 20_000)
- end
-
- @impl CandidateGenerator
- def audio_only_instances(_widgets, _config), do: []
-end
diff --git a/lib/screens/v2/candidate_generator/dup_new/departures.ex b/lib/screens/v2/candidate_generator/dup_new/departures.ex
deleted file mode 100644
index e9cb5f782..000000000
--- a/lib/screens/v2/candidate_generator/dup_new/departures.ex
+++ /dev/null
@@ -1,468 +0,0 @@
-defmodule Screens.V2.CandidateGenerator.DupNew.Departures do
- @moduledoc false
-
- import Screens.Inject
-
- alias Screens.Lines.Line
- alias Screens.Routes.Route
-
- alias Screens.V2.Departure
- alias Screens.V2.RDS
- alias Screens.V2.RDS.{Countdowns, FirstTrip, Headways, NoService, ServiceEnded}
-
- alias Screens.V2.WidgetInstance.Departures, as: DeparturesWidget
-
- alias Screens.V2.WidgetInstance.Departures.{
- HeadwayRow,
- HeadwaySection,
- NoDataSection,
- NormalSection,
- NoServiceSection,
- OvernightSection
- }
-
- alias Screens.V2.WidgetInstance.{DeparturesNoData, DeparturesNoService, OvernightDepartures}
-
- alias ScreensConfig.Departures
- alias ScreensConfig.Departures.{Header, Layout, Section}
- alias ScreensConfig.Screen
- alias ScreensConfig.Screen.Dup
-
- @type widget :: DeparturesNoData.t() | DeparturesWidget.t() | OvernightDepartures.t()
- @rds injected(RDS)
-
- @max_departures_per_rotation 4
-
- @primary_slot_names [
- :main_content_zero,
- :main_content_one,
- :main_content_reduced_zero,
- :main_content_reduced_one
- ]
- @secondary_slot_names [:main_content_two, :main_content_reduced_two]
-
- @spec instances(Screen.t(), DateTime.t()) :: [widget()]
- def instances(
- %Screen{
- app_params: %Dup{
- primary_departures: primary_departures,
- secondary_departures: secondary_departures
- }
- } = config,
- now
- ) do
- primary_rds_sections = @rds.get(primary_departures, now)
-
- secondary_rds_sections = @rds.get(secondary_departures, now)
-
- primary_departure_sections =
- create_departure_sections(primary_rds_sections, primary_departures)
-
- secondary_departure_sections =
- if secondary_rds_sections == [] or Enum.all?(secondary_rds_sections, &(&1 == {:ok, []})) do
- primary_departure_sections
- else
- create_departure_sections(secondary_rds_sections, secondary_departures)
- end
-
- all_sections_no_data =
- Enum.all?(
- primary_departure_sections ++ secondary_departure_sections,
- &is_struct(&1, NoDataSection)
- )
-
- all_sections_service_ended =
- Enum.all?(
- primary_departure_sections ++ secondary_departure_sections,
- &is_struct(&1, OvernightSection)
- )
-
- primary_instances =
- build_instances(
- @primary_slot_names,
- primary_departure_sections,
- all_sections_no_data,
- all_sections_service_ended,
- config,
- now
- )
-
- secondary_instances =
- build_instances(
- @secondary_slot_names,
- secondary_departure_sections,
- all_sections_no_data,
- all_sections_service_ended,
- config,
- now
- )
-
- primary_instances ++ secondary_instances
- end
-
- defp create_departure_sections(rds_sections, %Departures{sections: departure_sections}) do
- section_count = length(rds_sections)
-
- Enum.zip(rds_sections, departure_sections)
- |> Enum.map(fn {rds_section, %Section{bidirectional: bidirectional}} ->
- map_to_departure_section(rds_section, bidirectional, section_count)
- end)
- end
-
- @spec map_to_departure_section(RDS.section_t(), boolean(), number()) ::
- DeparturesWidget.section()
-
- defp map_to_departure_section(:error, _, _), do: %NoDataSection{}
-
- defp map_to_departure_section({:ok, []}, _, _), do: %NoDataSection{}
-
- defp map_to_departure_section({:ok, rds_list}, bidirectional, section_count) do
- num_departures_per_section = div(@max_departures_per_rotation, section_count)
-
- cond do
- no_service?(rds_list) ->
- %NoServiceSection{
- routes:
- rds_list
- |> Enum.flat_map(fn %RDS{state: %NoService{routes: routes}} -> routes end)
- |> Enum.uniq()
- }
-
- service_ended?(rds_list) ->
- %OvernightSection{
- routes:
- rds_list
- |> Enum.map(fn %RDS{
- state: %ServiceEnded{
- last_scheduled_departure: last_scheduled_departure
- }
- } ->
- Departure.route(last_scheduled_departure)
- end)
- |> Enum.uniq()
- }
-
- headways?(rds_list) ->
- create_headway_section(rds_list)
-
- true ->
- %NormalSection{
- rows:
- create_and_sort_rows(rds_list)
- |> maybe_make_bidirectional(bidirectional)
- |> Enum.take(num_departures_per_section),
- layout: %Layout{},
- header: %Header{}
- }
- end
- end
-
- defp no_service?(rds_list) do
- Enum.all?(rds_list, &is_struct(&1.state, NoService))
- end
-
- defp service_ended?(rds_list) do
- Enum.all?(rds_list, &is_struct(&1.state, ServiceEnded))
- end
-
- defp headways?(rds_list) do
- Enum.all?(rds_list, &is_struct(&1.state, Headways))
- end
-
- @spec create_headway_section([RDS.t()]) :: HeadwaySection.t()
-
- # Bidirectional -> Use no headsign for the trains message
- defp create_headway_section([
- %RDS{state: %{route_id: route_id, direction_id: direction_id_one, range: range}}
- | [%RDS{state: %{route_id: route_id, direction_id: direction_id_two, range: range}}]
- ])
- when direction_id_one != direction_id_two do
- %HeadwaySection{
- route: route_id,
- time_range: range,
- headsign: nil
- }
- end
-
- # Use the headsign if the destinations have the same headsign,
- # use the direction name if they have the same direction name,
- # otherwise default to no headsign
- defp create_headway_section(
- [
- %RDS{
- headsign: headsign,
- line: %Line{id: first_line_id},
- state: %Headways{
- route_id: route_id,
- direction_name: direction_name,
- direction_id: direction_id,
- range: range
- }
- }
- | _
- ] = destinations
- ) do
- %HeadwaySection{
- route: route_id,
- time_range: range,
- headsign:
- cond do
- Enum.all?(destinations, fn %RDS{headsign: other_headsign} ->
- headsign == other_headsign
- end) ->
- headsign
-
- Enum.all?(
- destinations,
- fn %RDS{
- line: %Line{id: other_line_id},
- state: %Headways{direction_id: other_direction_id}
- } ->
- first_line_id == other_line_id and direction_id == other_direction_id
- end
- ) ->
- direction_name
-
- true ->
- nil
- end
- }
- end
-
- defp build_instances(
- slot_names,
- _departure_sections,
- true = _all_section_no_data,
- _all_section_service_ended,
- config,
- _now
- ) do
- Enum.map(slot_names, &%DeparturesNoData{screen: config, slot_name: &1})
- end
-
- defp build_instances(
- slot_names,
- _departure_sections,
- _all_section_no_data,
- true = _all_section_service_ended,
- config,
- _now
- ) do
- Enum.map(slot_names, &%OvernightDepartures{screen: config, slot_names: [&1]})
- end
-
- defp build_instances(
- slot_names,
- departure_sections,
- _all_section_no_data,
- _all_section_service_ended,
- config,
- now
- ) do
- cond do
- Enum.all?(departure_sections, &is_struct(&1, NoServiceSection)) ->
- Enum.map(
- slot_names,
- &%DeparturesNoService{
- screen: config,
- slot_name: &1,
- routes:
- departure_sections
- |> Enum.flat_map(fn %NoServiceSection{routes: routes} -> routes end)
- |> Enum.map(fn route -> Route.icon(route) end)
- |> Enum.uniq()
- }
- )
-
- Enum.all?(departure_sections, &is_struct(&1, OvernightSection)) ->
- Enum.map(
- slot_names,
- &%OvernightDepartures{
- screen: config,
- slot_names: [&1],
- routes:
- departure_sections
- |> Enum.flat_map(fn %OvernightSection{routes: routes} -> routes end)
- |> Enum.map(fn route -> Route.icon(route) end)
- |> Enum.uniq()
- }
- )
-
- true ->
- Enum.map(
- slot_names,
- §ions_to_departure_widget(&1, departure_sections, config, now)
- )
- end
- end
-
- defp sections_to_departure_widget(slot_name, departure_sections, config, now) do
- %DeparturesWidget{
- screen: config,
- sections: departure_sections,
- slot_names: [slot_name],
- now: now
- }
- end
-
- @spec create_and_sort_rows([RDS.t()]) :: [NormalSection.row()]
- defp create_and_sort_rows(rds_list) do
- grouped_rds =
- Enum.group_by(rds_list, fn
- %RDS{state: %ServiceEnded{}} -> :service_ended
- %RDS{state: %Headways{}} -> :headways
- _ -> :other
- end)
-
- service_ended_rds = Map.get(grouped_rds, :service_ended, [])
- headway_rds = Map.get(grouped_rds, :headways, [])
- rds = Map.get(grouped_rds, :other, [])
-
- sorted_departures_from_rds(rds) ++
- headways_from_rds(rds ++ service_ended_rds, headway_rds) ++
- sorted_departures_from_rds(service_ended_rds, true)
- end
-
- @spec maybe_make_bidirectional([Departure.t()], boolean()) :: [Departure.t()]
- defp maybe_make_bidirectional([], _), do: []
- defp maybe_make_bidirectional(departures, false), do: departures
-
- defp maybe_make_bidirectional([first | rest], true) do
- first_direction = departure_direction_id(first)
-
- opposite? =
- Enum.find(rest, Enum.at(rest, 0), &(departure_direction_id(&1) == 1 - first_direction))
-
- Enum.reject([first, opposite?], &is_nil/1)
- end
-
- @spec sorted_departures_from_rds([RDS.t()], boolean()) :: [Departure.t()]
- defp sorted_departures_from_rds(rds, reverse \\ false) do
- sort_order =
- if reverse do
- :desc
- else
- :asc
- end
-
- rds
- |> Enum.flat_map(&departure_rows_from_state(&1))
- |> Enum.sort_by(
- &departure_time(&1),
- {
- sort_order,
- DateTime
- }
- )
- end
-
- @spec headways_from_rds([RDS.t()], [RDS.t()]) :: [HeadwayRow.t()]
- defp headways_from_rds(
- rds,
- headway_rds
- ) do
- # If there are other similar line/direction_id destinations that are in one of the other states,
- # disregard the headway for that particular destination
- lines_in_other_states =
- rds
- |> Enum.flat_map(&extract_line_direction_pairs/1)
- |> MapSet.new()
-
- headway_rds
- |> Enum.reject(fn %RDS{
- line: %Line{id: line_id},
- state: %Headways{direction_id: direction_id}
- } ->
- MapSet.member?(lines_in_other_states, {line_id, direction_id})
- end)
- |> Enum.group_by(fn %RDS{
- line: line,
- state: %Headways{direction_id: direction_id}
- } ->
- {line, direction_id}
- end)
- |> Enum.flat_map(fn {{line, direction_id}, rds_list} ->
- %RDS{
- headsign: headsign,
- state: %Headways{
- departure_id: departure_id,
- direction_name: direction_name,
- range: range
- }
- } = hd(rds_list)
-
- # If there are multiple headways with the same line but different headsigns,
- # combine them and use the direction name
- displayed_headsign = if length(rds_list) == 1, do: headsign, else: direction_name
-
- [
- %HeadwayRow{
- id: departure_id,
- line: line,
- direction_id: direction_id,
- range: range,
- headsign: displayed_headsign
- }
- ]
- end)
- end
-
- @spec departure_rows_from_state(RDS.t()) ::
- [Departure.t()] | [{Departure.t(), NormalSection.special_trip_type()}]
- defp departure_rows_from_state(%RDS{state: %Countdowns{departures: departures}}), do: departures
-
- defp departure_rows_from_state(%RDS{
- state: %FirstTrip{first_scheduled_departure: first_scheduled_departure}
- }) do
- [{first_scheduled_departure, :first_trip}]
- end
-
- defp departure_rows_from_state(%RDS{
- state: %ServiceEnded{last_scheduled_departure: last_scheduled_departure}
- }) do
- [{last_scheduled_departure, :last_trip}]
- end
-
- defp departure_rows_from_state(%RDS{state: %NoService{}}), do: []
-
- @spec departure_time(Departure.t()) :: DateTime.t()
- defp departure_time(%Departure{} = departure), do: Departure.time(departure)
-
- defp departure_time({first_scheduled_departure, :first_trip}),
- do: Departure.time(first_scheduled_departure)
-
- defp departure_time({last_scheduled_departure, :last_trip}),
- do: Departure.time(last_scheduled_departure)
-
- defp departure_direction_id(%Departure{} = departure), do: Departure.direction_id(departure)
-
- defp departure_direction_id({first_scheduled_departure, :first_trip}),
- do: Departure.direction_id(first_scheduled_departure)
-
- defp departure_direction_id({last_scheduled_departure, :last_trip}),
- do: Departure.direction_id(last_scheduled_departure)
-
- defp departure_direction_id(%HeadwayRow{direction_id: direction_id}), do: direction_id
-
- defp extract_line_direction_pairs(%RDS{line: %Line{id: line_id}, state: state}) do
- case state do
- %NoService{direction_id: nil} ->
- []
-
- %NoService{direction_id: direction_id} ->
- [{line_id, direction_id}]
-
- %Countdowns{departures: [first_departure | _]} ->
- [{line_id, Departure.direction_id(first_departure)}]
-
- %Countdowns{departures: [], direction_id: direction_id} ->
- [{line_id, direction_id}]
-
- %FirstTrip{first_scheduled_departure: departure} ->
- [{line_id, Departure.direction_id(departure)}]
-
- %ServiceEnded{last_scheduled_departure: departure} ->
- [{line_id, Departure.direction_id(departure)}]
- end
- end
-end
diff --git a/lib/screens/v2/candidate_generator/widgets/departures.ex b/lib/screens/v2/candidate_generator/widgets/departures.ex
index 38d2805ad..85d1eb130 100644
--- a/lib/screens/v2/candidate_generator/widgets/departures.ex
+++ b/lib/screens/v2/candidate_generator/widgets/departures.ex
@@ -75,7 +75,7 @@ defmodule Screens.V2.CandidateGenerator.Widgets.Departures do
section: &1,
result:
&1
- |> fetch_section_departures(disabled_modes, departure_fetch_fn)
+ |> fetch_section_departures(disabled_modes, departure_fetch_fn, now)
|> post_process_fn.(screen)
|> post_process_no_data(&1, has_multiple_sections, route_fetch_fn)
},
@@ -190,34 +190,19 @@ defmodule Screens.V2.CandidateGenerator.Widgets.Departures do
defp no_departures_message, do: "No departures currently available"
defp no_departures_message(direction_name), do: "No #{direction_name} departures available"
- @spec fetch_section_departures(Section.t()) :: Departure.result()
- @spec fetch_section_departures(Section.t(), [RouteType.t()]) :: Departure.result()
- @spec fetch_section_departures(Section.t(), [RouteType.t()], Departure.fetch()) ::
- Departure.result()
- @spec fetch_section_departures(Section.t(), [RouteType.t()], Departure.fetch(), DateTime.t()) ::
- Departure.result()
- def fetch_section_departures(
- _,
- disabled_route_types \\ [],
- departure_fetch_fn \\ &Departure.fetch/2,
- now \\ DateTime.utc_now()
- )
-
- def fetch_section_departures(%Section{header_only: true}, _, _, _) do
- {:ok, []}
- end
+ defp fetch_section_departures(%Section{header_only: true}, _, _, _), do: {:ok, []}
- def fetch_section_departures(
- %Section{
- query: %Query{params: params},
- filters: filters,
- bidirectional: is_bidirectional,
- grouping_type: grouping_type
- },
- disabled_route_types,
- departure_fetch_fn,
- now
- ) do
+ defp fetch_section_departures(
+ %Section{
+ query: %Query{params: params},
+ filters: filters,
+ bidirectional: is_bidirectional,
+ grouping_type: grouping_type
+ },
+ disabled_route_types,
+ departure_fetch_fn,
+ now
+ ) do
fetch_params = Map.from_struct(params)
fetch_opts = [schedule_route_type_filter: [:ferry, :rail]]
diff --git a/lib/screens/v2/departure.ex b/lib/screens/v2/departure.ex
index ef4c9efae..aab9b57b4 100644
--- a/lib/screens/v2/departure.ex
+++ b/lib/screens/v2/departure.ex
@@ -63,15 +63,22 @@ defmodule Screens.V2.Departure do
# Default to include all route_types, unless params or options include ones to filter on. If
# route_types to filter on are configured in params AND options, we only include route_types
# that are set in both.
- all_types = RouteType.all()
- opt_route_types = Keyword.get(opts, :schedule_route_type_filter, all_types)
- param_route_types = List.wrap(params[:route_type] || all_types)
-
- # An empty list here, which we'd intend as "no route types", would encode as an empty string
- # in the fetch params, which means "no route type *filter*" a.k.a. all route types. Fetching
- # schedules for "no route types" is just not fetching any schedules at all, so we can skip it.
- case Enum.filter(opt_route_types, &(&1 in param_route_types)) do
+ all_types = RouteType.all() |> Enum.sort()
+ param_types = (params[:route_type] || all_types) |> List.wrap() |> MapSet.new()
+ option_types = Keyword.get(opts, :schedule_route_type_filter, all_types) |> MapSet.new()
+
+ final_types =
+ param_types |> MapSet.intersection(option_types) |> MapSet.to_list() |> Enum.sort()
+
+ case final_types do
+ # An empty list here, which we'd intend as "no route types", would encode as an empty string
+ # in the fetch params, which means "no route type *filter*" a.k.a. all route types. Fetching
+ # schedules for "no route types" is just not fetching any schedules, so we can skip it.
[] -> {:ok, []}
+ # By similar logic, filtering on every route type is the same as not filtering at all, so
+ # normalize this to not include the filter.
+ ^all_types -> params |> Map.delete(:route_type) |> fetch_fn.()
+ # We have a final route type filter that is neither "everything" nor "nothing".
route_types -> params |> Map.put(:route_type, route_types) |> fetch_fn.()
end
end
@@ -176,10 +183,7 @@ defmodule Screens.V2.Departure do
def route(%__MODULE__{prediction: %Prediction{route: route}}), do: route
def route(%__MODULE__{prediction: nil, schedule: %Schedule{route: route}}), do: route
- def scheduled_time(%__MODULE__{schedule: s}) when not is_nil(s) do
- select_arrival_time(s)
- end
-
+ def scheduled_time(%__MODULE__{schedule: s}) when not is_nil(s), do: Schedule.time(s)
def scheduled_time(_), do: nil
@spec stop(t()) :: Stop.t()
@@ -206,16 +210,11 @@ defmodule Screens.V2.Departure do
prediction: %Prediction{arrival_time: nil, departure_time: nil},
schedule: s
}) do
- select_arrival_time(s)
+ Schedule.time(s)
end
- def time(%__MODULE__{prediction: p}) when not is_nil(p) do
- select_arrival_time(p)
- end
-
- def time(%__MODULE__{prediction: nil, schedule: s}) do
- select_arrival_time(s)
- end
+ def time(%__MODULE__{prediction: p}) when not is_nil(p), do: Prediction.time(p)
+ def time(%__MODULE__{prediction: nil, schedule: s}), do: Schedule.time(s)
def track_number(%__MODULE__{prediction: %Prediction{track_number: track_number}})
when not is_nil(track_number) do
@@ -262,9 +261,6 @@ defmodule Screens.V2.Departure do
defp crowding_level_from_occupancy_status(:full), do: 3
defp crowding_level_from_occupancy_status(nil), do: nil
- defp select_arrival_time(%{arrival_time: nil, departure_time: t}), do: t
- defp select_arrival_time(%{arrival_time: t, departure_time: _}), do: t
-
defp identify_stop_type_from_times(arrival_time, departure_time)
defp identify_stop_type_from_times(nil, _), do: :first_stop
defp identify_stop_type_from_times(_, nil), do: :last_stop
diff --git a/lib/screens/v2/rds.ex b/lib/screens/v2/rds.ex
index 2a733fe13..f765d6dec 100644
--- a/lib/screens/v2/rds.ex
+++ b/lib/screens/v2/rds.ex
@@ -17,6 +17,7 @@ defmodule Screens.V2.RDS do
alias Screens.Headways, as: Headway
alias Screens.LastTrip.LastTrip
alias Screens.Lines.Line
+ alias Screens.Report
alias Screens.RoutePatterns.RoutePattern
alias Screens.Routes.Route
alias Screens.RouteType
@@ -30,40 +31,19 @@ defmodule Screens.V2.RDS do
alias ScreensConfig.Departures
alias ScreensConfig.Departures.{Query, Section}
- alias __MODULE__.Countdowns
- alias __MODULE__.FirstTrip
- alias __MODULE__.NoService
- alias __MODULE__.ServiceEnded
-
- @type t ::
- %__MODULE__{
- stop: Stop.t(),
- line: Line.t(),
- headsign: String.t(),
- state: NoService.t() | Countdowns.t() | FirstTrip.t()
- }
+ alias __MODULE__.{Countdowns, FirstTrip, Headways, NoService, ServiceEnded}
+
+ @type state :: NoService.t() | Countdowns.t() | FirstTrip.t() | ServiceEnded.t() | Headways.t()
+
+ @type t :: %__MODULE__{stop: Stop.t(), line: Line.t(), headsign: String.t(), state: state()}
@enforce_keys ~w[stop line headsign state]a
defstruct @enforce_keys
- @type stop_id_result :: {:ok, [Stop.id()]} | :error
- @type data_results :: %{
- :alerts => Alert.result(),
- :child_stops => stop_id_result(),
- :departures => Departure.result(),
- :scheduled => Schedule.result(),
- :typical_patterns => RoutePattern.result()
- }
@type section_t :: {:ok, [t()]} | :error
- @type destination :: {Stop.t(), Line.t(), String.t()}
@type destination_key :: {Stop.id(), Line.id(), String.t()}
- @type rds_state :: NoService.t() | Countdowns.t() | FirstTrip.t() | ServiceEnded.t()
- @type service_state ::
- :before_scheduled_start
- | :after_scheduled_end
- | :active_period
- | :service_impacted
- | :no_service
- | :error
+
+ @typep destination :: {Stop.t(), Line.t(), String.t()}
+ @typep scheduled_service_state :: :after | :before | :none | :within
# These alert types eliminate service to a destination.
@relevant_alert_effects [
@@ -98,8 +78,8 @@ defmodule Screens.V2.RDS do
State when there is upcoming service to a destination
and/or alerts which affect service to the destination.
"""
- @type t :: %__MODULE__{departures: [Departure.t()], direction_id: Trip.direction() | nil}
- defstruct ~w[departures direction_id]a
+ @type t :: %__MODULE__{departures: [Departure.t(), ...]}
+ defstruct ~w[departures]a
end
defmodule FirstTrip do
@@ -108,8 +88,8 @@ defmodule Screens.V2.RDS do
showing the first scheduled trip of the day for a
given destination.
"""
- @type t :: %__MODULE__{first_scheduled_departure: Departure.t()}
- defstruct ~w[first_scheduled_departure]a
+ @type t :: %__MODULE__{first_schedule: Schedule.t()}
+ defstruct ~w[first_schedule]a
end
defmodule ServiceEnded do
@@ -117,8 +97,8 @@ defmodule Screens.V2.RDS do
State for after the end of the last scheduled departure
or if we observe a departure that is the Last Trip of the Day
"""
- @type t :: %__MODULE__{last_scheduled_departure: Departure.t()}
- defstruct ~w[last_scheduled_departure]a
+ @type t :: %__MODULE__{last_schedule: Schedule.t()}
+ defstruct ~w[last_schedule]a
end
defmodule Headways do
@@ -129,13 +109,12 @@ defmodule Screens.V2.RDS do
Shows an every “X-Y” minutes message.
"""
@type t :: %__MODULE__{
- departure_id: String.t(),
route_id: Route.id(),
direction_name: String.t(),
direction_id: Trip.direction(),
range: Headway.range()
}
- defstruct ~w[departure_id route_id direction_name direction_id range]a
+ defstruct ~w[route_id direction_name direction_id range]a
end
@alert injected(Alert)
@@ -169,25 +148,17 @@ defmodule Screens.V2.RDS do
)
when stop_ids != [] do
with %{
- typical_patterns: {:ok, typical_patterns},
- child_stops: {:ok, child_stops},
- scheduled: {:ok, scheduled},
- alerts: {:ok, alerts},
- departures: {:ok, departures}
+ alerts: alerts,
+ child_stops: child_stops,
+ departures: departures,
+ schedules: schedules,
+ typical_patterns: typical_patterns
} <- fetch_data(params, now) do
- scheduled_departures =
- Enum.map(scheduled, fn schedule -> %Departure{prediction: nil, schedule: schedule} end)
-
- case create_routes_for_section(
- departures,
- scheduled_departures,
- typical_patterns,
- params
- ) do
+ case create_routes_for_section(departures, schedules, typical_patterns, params) do
{[_ | _] = enabled_routes_for_section, _} ->
create_section_rds(
departures,
- scheduled_departures,
+ schedules,
typical_patterns,
child_stops,
enabled_routes_for_section,
@@ -199,12 +170,21 @@ defmodule Screens.V2.RDS do
{:ok, []}
_ ->
+ Report.warning("rds_no_section_routes", params: inspect(params))
:error
end
end
end
- @spec fetch_data(Query.Params.t(), DateTime.t()) :: data_results()
+ @spec fetch_data(Query.Params.t(), DateTime.t()) ::
+ %{
+ :alerts => [Alert.t()],
+ :child_stops => [Stop.t()],
+ :departures => [Departure.t()],
+ :schedules => [Schedule.t()],
+ :typical_patterns => [RoutePattern.t()]
+ }
+ | :error
defp fetch_data(params, now) do
data_fetch_fns = [
typical_patterns: fn params, _now ->
@@ -213,35 +193,33 @@ defmodule Screens.V2.RDS do
child_stops: fn %Query.Params{stop_ids: stop_ids} = _params, _now ->
fetch_child_stops(stop_ids)
end,
- scheduled: fn %Query.Params{stop_ids: stop_ids} = _params, _now ->
- @schedule.fetch(%{stop_ids: stop_ids}, Util.service_date(now))
+ schedules: fn params, _now ->
+ @schedule.fetch(Map.from_struct(params), Util.service_date(now))
end,
alerts: fn %Query.Params{stop_ids: stop_ids} = _params, _now ->
fetch_relevant_alerts(stop_ids)
end,
departures: fn params, _now ->
- params |> Map.from_struct() |> @departure.fetch(now: now)
+ params
+ |> Map.from_struct()
+ |> @departure.fetch(now: now, include_scheduled_cancelled?: true)
end
]
data_fetch_fns
|> Task.async_stream(fn {key, func} -> {key, func.(params, now)} end, timeout: 30_000)
- |> Enum.into(%{}, fn {:ok, {key, value}} -> {key, value} end)
- |> then(fn results ->
- if Enum.all?(results, fn {_key, value} -> match?({:ok, _}, value) end) do
- results
- else
- :error
- end
+ |> Enum.reduce_while(%{}, fn
+ {:ok, {key, {:ok, data}}}, results -> {:cont, Map.put(results, key, data)}
+ {:ok, {_key, _error}}, _results -> {:halt, :error}
end)
end
- @spec fetch_child_stops([Stop.id()]) :: stop_id_result()
+ @spec fetch_child_stops([Stop.id()]) :: {:ok, [Stop.t()]} | :error
defp fetch_child_stops(stop_ids) do
with {:ok, stops} <- @stop.fetch(%{ids: stop_ids}, _include_related? = true) do
stops_by_id = Map.new(stops, fn %Stop{id: id} = stop -> {id, stop} end)
- child_stop_ids =
+ child_stops =
stop_ids
|> Enum.map(&stops_by_id[&1])
|> Enum.flat_map(fn
@@ -251,13 +229,13 @@ defmodule Screens.V2.RDS do
nil -> []
end)
- {:ok, child_stop_ids}
+ {:ok, child_stops}
end
end
defp create_section_rds(
departures,
- scheduled_departures,
+ schedules,
typical_patterns,
child_stops,
routes_for_section,
@@ -265,7 +243,7 @@ defmodule Screens.V2.RDS do
now
) do
departures_by_destination = group_by_destination(departures)
- scheduled_departures_by_destination = group_by_destination(scheduled_departures)
+ schedules_by_destination = group_by_destination(schedules)
destinations =
(tuples_from_departures(departures, now) ++
@@ -277,66 +255,35 @@ defmodule Screens.V2.RDS do
section_rds =
destinations
- |> Enum.map(fn destination ->
- create_destination_rds(
- destination,
- departures_by_destination,
- scheduled_departures_by_destination,
- routes_for_section,
- impacted_destinations,
- now
- )
+ |> Enum.map(fn {%Stop{id: stop_id} = stop, line, headsign} = destination ->
+ key = to_destination_key(destination)
+
+ %__MODULE__{
+ stop: stop,
+ line: line,
+ headsign: headsign,
+ state:
+ destination_state(
+ Map.get(departures_by_destination, key, []),
+ Map.get(schedules_by_destination, key, []),
+ @headways.get(stop_id, now),
+ routes_for_section,
+ last_trip_departed?(key, now),
+ destination in impacted_destinations,
+ now
+ )
+ }
end)
|> Enum.reject(&is_nil(&1.state))
{:ok, section_rds}
end
- defp create_destination_rds(
- {%Stop{id: stop_id} = stop, %Line{id: line_id} = line, headsign} = destination,
- departures_by_destination,
- scheduled_departures_by_destination,
- routes_for_section,
- impacted_destinations,
- now
- ) do
- headway_for_stop = @headways.get(stop_id, now)
- destination_key = {stop_id, line_id, headsign}
-
- departures =
- Map.get(departures_by_destination, destination_key, [])
- |> Enum.filter(fn
- %{prediction: nil} -> headway_for_stop == nil
- _ -> true
- end)
-
- scheduled_departures =
- scheduled_departures_by_destination |> Map.get(destination_key, [])
-
- impacted_by_alert = destination in impacted_destinations
-
- %__MODULE__{
- stop: stop,
- line: line,
- headsign: headsign,
- state:
- state(
- departures,
- scheduled_departures,
- destination_key,
- routes_for_section,
- headway_for_stop,
- impacted_by_alert,
- now
- )
- }
- end
-
@spec tuples_from_departures([Departure.t()], DateTime.t()) :: [destination()]
defp tuples_from_departures(departures, now) do
departures
|> Enum.filter(&(DateTime.diff(Departure.time(&1), now, :minute) <= @max_departure_minutes))
- |> Enum.map(&destination_from_departure(&1))
+ |> Enum.map(&departure_destination(&1))
end
@spec tuples_from_patterns([RoutePattern.t()], [Stop.id()]) :: [destination()]
@@ -354,103 +301,90 @@ defmodule Screens.V2.RDS do
)
end
- @spec state(
- [Departure.t()],
+ @spec destination_state(
[Departure.t()],
- destination_key(),
- [Route.t()],
+ [Schedule.t()],
Headway.range() | nil,
+ [Route.t()],
+ boolean(),
boolean(),
DateTime.t()
- ) :: rds_state()
- defp state(
- [] = _departures_for_headsign,
- [] = _scheduled_departures_for_headsign,
- _destination_key,
- routes_for_section,
- _headway_for_stop,
- false = _impacted_by_alert,
- _now
- ) do
- %NoService{routes: routes_for_section}
- end
-
- defp state(
- [] = departures_for_headsign,
- [_ | _] = scheduled_departures_for_headsign,
- destination_key,
+ ) :: state() | nil
+ defp destination_state(
+ departures,
+ schedules,
+ headways,
routes_for_section,
- headway_for_stop,
- impacted_by_alert,
+ last_trip_departed?,
+ impacted_by_alert?,
now
) do
- {first_scheduled_departure, last_scheduled_departure} =
- scheduled_departures_for_headsign
- |> Enum.sort_by(&Departure.time(&1), DateTime)
+ {first_schedule, last_schedule} =
+ schedules
+ |> Enum.sort_by(&Schedule.time(&1), DateTime)
|> then(&{List.first(&1), List.last(&1)})
- case classify_service_state(
- first_scheduled_departure,
- last_scheduled_departure,
- headway_for_stop,
- impacted_by_alert,
- after_last_trip?(destination_key, now),
- now
- ) do
- :before_scheduled_start ->
- %FirstTrip{first_scheduled_departure: first_scheduled_departure}
-
- :after_scheduled_end ->
- %ServiceEnded{last_scheduled_departure: last_scheduled_departure}
-
- :service_impacted ->
- %Countdowns{
- departures: departures_for_headsign,
- direction_id: Departure.direction_id(first_scheduled_departure)
- }
+ presented_departures =
+ if not is_nil(headways) or impacted_by_alert?,
+ do: Enum.reject(departures, &is_nil(&1.prediction)),
+ else: departures
- :no_service ->
- %NoService{routes: routes_for_section}
+ service_state =
+ scheduled_service_state(first_schedule, last_schedule, headways, last_trip_departed?, now)
- :active_period ->
- route = Departure.route(first_scheduled_departure)
- direction_id = Departure.direction_id(first_scheduled_departure)
+ cond do
+ presented_departures != [] -> %Countdowns{departures: presented_departures}
+ impacted_by_alert? -> nil
+ service_state == :none -> %NoService{routes: routes_for_section}
+ service_state == :before -> %FirstTrip{first_schedule: first_schedule}
+ service_state == :after -> %ServiceEnded{last_schedule: last_schedule}
+ not is_nil(headways) -> headways_state(headways, first_schedule)
+ true -> nil
+ end
+ end
- %Headways{
- departure_id: Departure.id(first_scheduled_departure),
- route_id: route.id,
- direction_name: route |> Route.normalized_direction_names() |> Enum.at(direction_id),
- direction_id: Departure.direction_id(first_scheduled_departure),
- range: headway_for_stop
- }
+ defp headways_state(headways, %Schedule{
+ route: %Route{id: route_id} = route,
+ trip: %Trip{direction_id: direction_id}
+ }) do
+ %Headways{
+ route_id: route_id,
+ direction_name: route |> Route.normalized_direction_names() |> Enum.at(direction_id),
+ direction_id: direction_id,
+ range: headways
+ }
+ end
- :error ->
- Logster.warning([
- "rds_state_creation_failed",
- destination_key: destination_key,
- now: now,
- first_scheduled_departure: first_scheduled_departure.schedule.id,
- last_scheduled_departure: last_scheduled_departure.schedule.id
- ])
+ @spec scheduled_service_state(
+ Schedule.t() | nil,
+ Schedule.t() | nil,
+ Headway.range() | nil,
+ boolean(),
+ DateTime.t()
+ ) :: scheduled_service_state()
+ defp scheduled_service_state(nil = _first, nil = _last, _headways, _last_departed?, _now),
+ do: :none
- nil
+ defp scheduled_service_state(_first, _last, _headways, true = _last_departed?, _now),
+ do: :after
+
+ defp scheduled_service_state(first_schedule, last_schedule, headways, _last_departed?, now) do
+ effective_start = first_schedule |> Schedule.time() |> effective_service_time(headways)
+ effective_end = last_schedule |> Schedule.time() |> effective_service_time(headways)
+
+ cond do
+ DateTime.compare(now, effective_start) == :lt -> :before
+ DateTime.compare(now, effective_end) == :gt -> :after
+ true -> :within
end
end
- defp state(
- departures_for_headsign,
- _scheduled_departures_by_headsign,
- _destination_key,
- _routes_for_section,
- _headway_for_stop,
- _impacted_by_alert,
- _now
- ) do
- %Countdowns{departures: departures_for_headsign}
- end
+ defp effective_service_time(nil, _headways), do: nil
+ defp effective_service_time(time, nil), do: time
+ defp effective_service_time(time, {_low, high}), do: DateTime.add(time, -high, :minute)
- @spec after_last_trip?(destination_key(), DateTime.t()) :: boolean()
- defp after_last_trip?({stop_id, _line_id, headsign} = destination_key, now) do
+ @spec last_trip_departed?(destination_key(), DateTime.t()) :: boolean()
+ defp last_trip_departed?({stop_id, _line_id, headsign} = destination_key, now) do
departure_times = @last_trip.last_trip_departure_times(destination_key)
case {red_trunk_to_alewife?(stop_id, headsign), departure_times} do
@@ -466,34 +400,41 @@ defmodule Screens.V2.RDS do
|> after_last_trip_with_buffer?(now)
end
- @spec group_by_destination([Departure.t()]) :: %{destination_key() => [Departure.t()]}
- defp group_by_destination(departures) do
- Enum.group_by(departures, fn departure ->
- departure
- |> destination_from_departure()
- |> then(fn {%Stop{id: stop_id}, %Line{id: line_id}, headsign} ->
- {stop_id, line_id, headsign}
- end)
+ @spec group_by_destination([item]) :: %{destination_key() => [item]}
+ when item: Departure.t() | Schedule.t()
+ defp group_by_destination(items) do
+ Enum.group_by(items, fn
+ %Departure{} = d -> d |> departure_destination() |> to_destination_key()
+ %Schedule{} = s -> s |> schedule_destination() |> to_destination_key()
end)
end
- @spec destination_from_departure(Departure.t()) :: destination()
- defp destination_from_departure(departure) do
+ @spec departure_destination(Departure.t()) :: destination()
+ defp departure_destination(%Departure{} = departure) do
{Departure.stop(departure), Departure.route(departure).line,
Departure.representative_headsign(departure)}
end
+ defp schedule_destination(%Schedule{route: %Route{line: line}, trip: trip, stop: stop}) do
+ {stop, line, Trip.representative_headsign(trip)}
+ end
+
+ @spec to_destination_key(destination()) :: destination_key()
+ defp to_destination_key({%Stop{id: stop_id}, %Line{id: line_id}, headsign}),
+ do: {stop_id, line_id, headsign}
+
defp create_routes_for_section(
departures,
- scheduled_departures,
+ schedules,
typical_patterns,
%Query.Params{route_ids: route_id_params, route_type: route_type} = _params
) do
routes_for_section =
- (departures ++ scheduled_departures ++ typical_patterns)
+ (departures ++ schedules ++ typical_patterns)
|> Enum.map(fn
%Departure{} = departure -> Departure.route(departure)
%RoutePattern{route: route} -> route
+ %Schedule{route: route} -> route
end)
|> Enum.uniq()
|> filter_for_route_id_params(route_id_params)
@@ -522,81 +463,6 @@ defmodule Screens.V2.RDS do
defp reject_disabled_modes(all_routes, disabled_modes),
do: Enum.reject(all_routes, fn route -> route.type in disabled_modes end)
- @spec classify_service_state(
- Departure.t(),
- Departure.t(),
- Headway.range() | nil,
- boolean(),
- boolean(),
- DateTime.t()
- ) :: service_state()
- defp classify_service_state(
- _first_departure,
- _last_departure,
- _headway_for_stop,
- true,
- _after_last_trip,
- _now
- ),
- do: :service_impacted
-
- defp classify_service_state(
- _first_departure,
- _last_departure,
- _headway_for_stop,
- _in_alert,
- true,
- _now
- ),
- do: :after_scheduled_end
-
- defp classify_service_state(
- first_departure,
- last_departure,
- _headway_for_stop,
- _in_alert,
- _after_last_trip,
- _now
- )
- when first_departure == nil and last_departure == nil,
- do: :no_service
-
- defp classify_service_state(
- first_departure,
- last_departure,
- headway_for_stop,
- _in_alert,
- _after_last_trip,
- now
- ) do
- first_departure_time =
- case headway_for_stop do
- nil ->
- Departure.time(first_departure)
-
- {_low, high} ->
- first_departure |> Departure.time() |> DateTime.add(-high, :minute)
- end
-
- last_departure_time = Departure.time(last_departure)
-
- cond do
- DateTime.compare(now, first_departure_time) == :lt and
- Util.service_date(now) == Util.service_date(Departure.time(first_departure)) ->
- :before_scheduled_start
-
- DateTime.compare(now, last_departure_time) == :gt and
- Util.service_date(now) == Util.service_date(Departure.time(last_departure)) ->
- :after_scheduled_end
-
- headway_for_stop != nil ->
- :active_period
-
- true ->
- :error
- end
- end
-
@spec fetch_relevant_alerts([Stop.id()]) :: {:ok, [Alert.t()]} | :error
defp fetch_relevant_alerts(stop_ids) do
with {:ok, alerts} <-
@@ -611,11 +477,13 @@ defmodule Screens.V2.RDS do
defp relevant_alert_effect?(_), do: false
- @spec informed_destinations([destination()], [Alert.t()], [RoutePattern.t()]) :: [destination()]
+ @spec informed_destinations([destination()], [Alert.t()], [RoutePattern.t()]) ::
+ MapSet.t(destination())
defp informed_destinations(destinations, alerts, typical_patterns) do
# Filters destinations to return only those that are affected by at least one alert.
# Stops checking as soon as an alert is found that affects the destination.
- Enum.filter(destinations, fn {stop, _line, _headsign} = destination ->
+ destinations
+ |> Enum.filter(fn {stop, _line, _headsign} = destination ->
case pattern_for_destination(destination, typical_patterns) do
nil ->
nil
@@ -628,6 +496,7 @@ defmodule Screens.V2.RDS do
end)
end
end)
+ |> MapSet.new()
end
@spec pattern_for_destination(destination(), [RoutePattern.t()]) :: RoutePattern.t() | nil
diff --git a/lib/screens/v2/screen_data/parameters.ex b/lib/screens/v2/screen_data/parameters.ex
index bfef02480..3c52c1dd4 100644
--- a/lib/screens/v2/screen_data/parameters.ex
+++ b/lib/screens/v2/screen_data/parameters.ex
@@ -33,8 +33,7 @@ defmodule Screens.V2.ScreenData.Parameters do
},
dup_v2: %Static{
candidate_generator: CandidateGenerator.Dup,
- refresh_rate: 20,
- variants: %{"new_departures" => CandidateGenerator.DupNew}
+ refresh_rate: 20
},
gl_eink_v2: %Static{
audio_active_time: @all_times,
diff --git a/lib/screens/v2/widget_instance/departures.ex b/lib/screens/v2/widget_instance/departures.ex
index bf422bef7..915ce0b82 100644
--- a/lib/screens/v2/widget_instance/departures.ex
+++ b/lib/screens/v2/widget_instance/departures.ex
@@ -23,7 +23,6 @@ defmodule Screens.V2.WidgetInstance.Departures do
defmodule HeadwayRow do
@moduledoc "A row that shows headway values in a NormalSection, X every Y - Z minutes"
@type t :: %__MODULE__{
- id: String.t(),
line: Line.t(),
direction_id: Trip.direction(),
range: Headways.range(),
@@ -36,11 +35,11 @@ defmodule Screens.V2.WidgetInstance.Departures do
defmodule NormalSection do
@moduledoc "Section which includes a number of independent 'rows' or items."
- @type special_trip_type :: :first_trip | :last_trip
+ @type special_trip :: {Schedule.t(), :first_trip | :service_ended}
@type row ::
Departure.t()
- | {Departure.t(), special_trip_type()}
+ | special_trip()
| HeadwayRow.t()
| FreeTextLine.t()
@@ -401,12 +400,8 @@ defmodule Screens.V2.WidgetInstance.Departures do
defp row_departure_grouping(%Departure{} = row),
do: {Departure.route(row), Departure.headsign(row)}
- defp row_departure_grouping({first_scheduled_departure, :first_trip}),
- do:
- {Departure.route(first_scheduled_departure), Departure.headsign(first_scheduled_departure)}
-
- defp row_departure_grouping({last_scheduled_departure, :last_trip}),
- do: {Departure.route(last_scheduled_departure), Departure.headsign(last_scheduled_departure)}
+ defp row_departure_grouping({%Schedule{route: route} = schedule, _special_trip_type}),
+ do: {route, Schedule.headsign(schedule)}
defp row_departure_grouping(%FreeTextLine{}), do: make_ref()
@@ -463,60 +458,31 @@ defmodule Screens.V2.WidgetInstance.Departures do
end
defp serialize_departure_group(
- [{first_scheduled_departure, :first_trip}],
+ [{%Schedule{id: id} = schedule, special_trip_type}],
screen,
now,
route_pill_serializer
) do
- row_id =
- first_scheduled_departure
- |> Departure.id()
- |> hash_and_encode()
-
- departures = [first_scheduled_departure]
+ departures = [%Departure{schedule: schedule}]
%{
- id: row_id,
+ id: hash_and_encode(id),
type: :departure_row,
route: serialize_route(departures, route_pill_serializer, screen),
headsign: serialize_headsign(departures, screen),
- times_with_crowding: serialize_times_with_crowding(departures, screen, now),
+ times_with_crowding:
+ case special_trip_type do
+ :first_trip -> serialize_times_with_crowding(departures, screen, now)
+ :service_ended -> [%{id: id, time: %{type: :overnight, is_live: false}}]
+ end,
direction_id: serialize_direction_id(departures),
- is_first_trip: true
- }
- end
-
- # Overnight Row specifically here instead of showing OvernightSection or OvernightDepartures,
- # which indicates there are other destinations in the section with upcoming departures.
- defp serialize_departure_group(
- [{last_scheduled_departure, :last_trip}],
- screen,
- _now,
- route_pill_serializer
- ) do
- departure_id = Departure.id(last_scheduled_departure)
-
- departures = [last_scheduled_departure]
-
- %{
- id: hash_and_encode(departure_id),
- type: :departure_row,
- route: serialize_route(departures, route_pill_serializer, screen),
- headsign: serialize_headsign(departures, screen),
- times_with_crowding: [
- %{
- id: departure_id,
- time: %{type: :overnight}
- }
- ],
- direction_id: serialize_direction_id(departures)
+ is_first_trip: special_trip_type == :first_trip
}
end
defp serialize_departure_group(
[
%HeadwayRow{
- id: id,
line: line,
direction_id: direction_id,
range: {lo, hi},
@@ -528,8 +494,10 @@ defmodule Screens.V2.WidgetInstance.Departures do
_now,
route_pill_serializer
) do
+ id = hash_and_encode(line.id <> headsign)
+
%{
- id: hash_and_encode(id),
+ id: id,
type: :departure_row,
route: route_pill_serializer.(line, nil, screen),
headsign: %{headsign: headsign},
@@ -645,12 +613,12 @@ defmodule Screens.V2.WidgetInstance.Departures do
defp serialize_crowding(departure, _screen), do: Departure.crowding_level(departure)
@spec serialize_time(Departure.t(), Screen.t(), DateTime.t()) ::
- %{time: serialized_time(), time_in_epoch: integer(), is_live: boolean()}
- | %{
- time: serialized_time() | nil,
- scheduled_time: serialized_timestamp() | nil,
- is_live: boolean()
- }
+ %{
+ :time => serialized_time() | nil,
+ optional(:time_in_epoch) => integer(),
+ optional(:scheduled_time) => serialized_timestamp() | nil,
+ :is_live => boolean()
+ }
defp serialize_time(departure, %Screen{app_id: app_id}, now)
when app_id in [:bus_eink_v2, :gl_eink_v2] do
departure_time = Departure.time(departure)
@@ -669,39 +637,36 @@ defmodule Screens.V2.WidgetInstance.Departures do
%{time: time, time_in_epoch: DateTime.to_unix(departure_time), is_live: false}
end
- defp serialize_time(
- %Departure{schedule: %Schedule{arrival_time: nil, departure_time: nil}},
- _screen,
- _now
- ),
- do: %{time: %{type: :overnight}, is_live: false}
-
defp serialize_time(%Departure{prediction: prediction} = departure, screen, now) do
scheduled_time = Departure.scheduled_time(departure)
%Route{type: route_type} = Departure.route(departure)
- include_scheduled_time? = route_type in [:ferry, :rail]
+ always_timestamp? = route_type in [:ferry, :rail]
predicted? = not is_nil(prediction)
- serialized_time =
+ serialized_predicted_time =
cond do
Departure.cancelled?(departure) -> nil
- predicted? and not include_scheduled_time? -> serialize_realtime(departure, screen, now)
+ predicted? and not always_timestamp? -> serialize_realtime(departure, screen, now)
true -> departure |> Departure.time() |> serialize_timestamp()
end
+ # Only the DUP app supports displaying the originally-scheduled time of a departure, either
+ # alongside a predicted time or as a "cancelled" time. Guard against use on other screens so,
+ # in the latter case, the client will crash rather than show misleading data (but this should
+ # be handled by "producers" of this widget not including cancelled departures at all).
serialized_scheduled_time =
- if include_scheduled_time? and not is_nil(scheduled_time) do
- serialized_scheduled_time = serialize_timestamp(scheduled_time)
-
- case serialized_time do
- %{type: :text} -> nil
- ^serialized_scheduled_time -> nil
- _ -> serialized_scheduled_time
- end
+ with %Screen{app_id: :dup_v2} <- screen,
+ true <- not is_nil(scheduled_time),
+ time when is_nil(time) or time.type == :timestamp <- serialized_predicted_time,
+ serialized = serialize_timestamp(scheduled_time),
+ true <- serialized != serialized_predicted_time do
+ serialized
+ else
+ _ -> nil
end
%{
- time: serialized_time,
+ time: serialized_predicted_time,
scheduled_time: serialized_scheduled_time,
is_live: predicted?
}
diff --git a/lib/screens/vehicles/vehicle.ex b/lib/screens/vehicles/vehicle.ex
index a7b9429bd..a5960fa48 100644
--- a/lib/screens/vehicles/vehicle.ex
+++ b/lib/screens/vehicles/vehicle.ex
@@ -1,9 +1,7 @@
defmodule Screens.Vehicles.Vehicle do
@moduledoc false
- alias Screens.Routes.Route
alias Screens.Trips.Trip
- alias Screens.V3Api.Parser
alias Screens.Vehicles.Carriage
defstruct id: nil,
@@ -36,17 +34,4 @@ defmodule Screens.Vehicles.Vehicle do
occupancy_status: occupancy_status,
carriages: list(Carriage.t())
}
-
- @type by_route_and_direction :: (Route.id(), Trip.direction() -> [t()])
-
- @spec by_route_and_direction(Route.id(), Trip.direction()) :: [t()]
- def by_route_and_direction(route_id, direction_id) do
- case Screens.V3Api.get_json("vehicles", %{
- "filter[route]" => route_id,
- "filter[direction_id]" => direction_id
- }) do
- {:ok, result} -> result |> Parser.parse() |> Enum.reject(&is_nil(&1.stop_id))
- _ -> []
- end
- end
end
diff --git a/lib/screens_web/controllers/v2/screen_api_controller.ex b/lib/screens_web/controllers/v2/screen_api_controller.ex
index b3e67c28c..cb78de713 100644
--- a/lib/screens_web/controllers/v2/screen_api_controller.ex
+++ b/lib/screens_web/controllers/v2/screen_api_controller.ex
@@ -4,7 +4,7 @@ defmodule ScreensWeb.V2.ScreenApiController do
alias Phoenix.View
alias Screens.V2.{ScreenAudioData, ScreenData}
alias ScreensConfig.Screen
- alias ScreensWeb.Plug.{ScreenRequest, VariantCanary}
+ alias ScreensWeb.Plug.ScreenRequest
import Screens.Inject
@cache injected(Screens.Config.Cache)
@@ -17,7 +17,6 @@ defmodule ScreensWeb.V2.ScreenApiController do
plug Corsica, [origins: "*"] when action in [:show_dup, :log_frontend_error]
plug ScreenRequest, [type: :data] when action in @non_pending_show_actions
plug ScreenRequest, [type: :data, pending?: true] when action in @pending_show_actions
- plug VariantCanary when action in @non_pending_show_actions
plug :disabled_response when action in @non_pending_show_actions
plug :outdated_response when action in @non_pending_show_actions
diff --git a/lib/screens_web/plug/variant_canary.ex b/lib/screens_web/plug/variant_canary.ex
deleted file mode 100644
index a0e50da6c..000000000
--- a/lib/screens_web/plug/variant_canary.ex
+++ /dev/null
@@ -1,32 +0,0 @@
-defmodule ScreensWeb.Plug.VariantCanary do
- @moduledoc """
- Randomizes a percentage of "real screen" data requests that do not specify a variant to instead
- use a specific variant under testing. Expects to be run after `ScreenRequest` in the pipeline.
- """
-
- alias Plug.Conn
- alias ScreensConfig.Screen
-
- @app_id :dup_v2
- @is_enabled Mix.env() != :test
- @percentage 1
- @variant "new_departures"
-
- def init(options), do: options
-
- def call(
- %Conn{assigns: %{is_real_screen: true, screen: %Screen{app_id: @app_id}, variant: nil}} =
- conn,
- _options
- ) do
- if @is_enabled and :rand.uniform() * 100 < @percentage do
- # Update metadata previously set in `ScreenRequest` so request logging remains accurate
- Logger.metadata(variant: @variant)
- Conn.assign(conn, :variant, @variant)
- else
- conn
- end
- end
-
- def call(conn, _options), do: conn
-end
diff --git a/test/screens/headways_test.exs b/test/screens/headways_test.exs
index 5e8d7b51d..12dceaf2b 100644
--- a/test/screens/headways_test.exs
+++ b/test/screens/headways_test.exs
@@ -20,7 +20,6 @@ defmodule Screens.HeadwaysTest do
@sl_one_terminal_a "17092"
@sl_two_drydock_ave "31259"
@sl_three_airport "7096"
- @congress_st_at_wtc "17096"
test "returns nil for a stop with no defined headways" do
assert Headways.get("nonexistent", local_dt()) == nil
@@ -46,21 +45,4 @@ defmodule Screens.HeadwaysTest do
assert Headways.get(@blue_trunk, local_dt(7)) == {12, 16}
end
end
-
- describe "get_with_route/3" do
- test "returns the correct value for a combination of parent station and route for Rail" do
- assert Headways.get_with_route("place-aport", "Blue", local_dt()) == {9, 13}
- assert Headways.get_with_route("place-north", "Green-D", local_dt()) == {7, 13}
- assert Headways.get_with_route("place-north", "Orange", local_dt()) == {9, 11}
- end
-
- test "returns the correct value for a combination of station id and route for the Silver Line" do
- assert Headways.get_with_route("place-chels", "743", local_dt()) == {7, 9}
- assert Headways.get_with_route("place-crtst", "746", local_dt()) == {1, 3}
- assert Headways.get_with_route("place-crtst", "743", local_dt()) == {1, 3}
- assert Headways.get_with_route(@congress_st_at_wtc, "742", local_dt()) == {1, 3}
- assert Headways.get_with_route(@congress_st_at_wtc, "743", local_dt()) == {1, 3}
- assert Headways.get_with_route("place-crtst", "751", local_dt()) == nil
- end
- end
end
diff --git a/test/screens/v2/candidate_generator/dup/departures_headway_test.exs b/test/screens/v2/candidate_generator/dup/departures_headway_test.exs
deleted file mode 100644
index 49a37bcd1..000000000
--- a/test/screens/v2/candidate_generator/dup/departures_headway_test.exs
+++ /dev/null
@@ -1,606 +0,0 @@
-defmodule Screens.V2.CandidateGenerator.Dup.DeparturesHeadwayTest do
- use ExUnit.Case, async: true
-
- alias Screens.Alerts.Alert
- alias Screens.Predictions.Prediction
- alias Screens.Routes.Route
- alias Screens.Stops.Stop
- alias Screens.Trips.Trip
- alias Screens.V2.CandidateGenerator.Dup
- alias Screens.V2.Departure
- alias Screens.V2.WidgetInstance.Departures, as: DeparturesWidget
- alias Screens.V2.WidgetInstance.Departures.{HeadwaySection, NormalSection}
- alias Screens.Vehicles.Vehicle
- alias ScreensConfig.{Alerts, Departures, Header}
- alias ScreensConfig.Departures.Header, as: SectionHeader
- alias ScreensConfig.Departures.{Layout, Query, Section}
- alias ScreensConfig.Screen
- alias ScreensConfig.Screen.Dup, as: DupConfig
-
- import Screens.Inject
- import Screens.TestSupport.InformedEntityBuilder
- import Mox
- setup :verify_on_exit!
-
- @headways injected(Screens.Headways)
-
- defp put_primary_departures(widget, primary_departures_sections) do
- %{
- widget
- | app_params: %{
- widget.app_params
- | primary_departures: %Departures{sections: primary_departures_sections}
- }
- }
- end
-
- setup do
- config = %Screen{
- app_params: %DupConfig{
- header: %Header.StopId{stop_id: "place-headway-test"},
- primary_departures: %Departures{
- sections: []
- },
- secondary_departures: %Departures{
- sections: []
- },
- alerts: struct(Alerts)
- },
- vendor: :outfront,
- device_id: "TEST",
- name: "TEST",
- app_id: :dup_v2
- }
-
- fetch_departures_fn = fn
- %{stop_ids: ["place-A"]}, _opts ->
- {:ok,
- [
- %Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- )
- }
- ]}
-
- _, _ ->
- {:ok, []}
- end
-
- fetch_alerts_fn = fn
- _ -> []
- end
-
- fetch_schedules_fn = fn
- _, _ ->
- []
- end
-
- fetch_vehicles_fn = fn _, _ -> [struct(Vehicle)] end
-
- fetch_routes_fn = fn
- %{ids: ids} ->
- {
- :ok,
- ids
- |> Enum.flat_map(fn
- "Ferry" ->
- [%{id: "Ferry", type: :ferry}]
-
- "Orange" ->
- [%{id: "Orange", type: :subway}]
-
- "Green" ->
- [%{id: "Green", type: :light_rail}]
-
- "Red" ->
- [%{id: "Red", type: :subway}]
-
- "Blue" ->
- [
- %Route{
- id: "Blue",
- type: :subway,
- direction_names: ["Test Direction Zero", "Test Direction One"]
- }
- ]
-
- "743" ->
- [%Route{id: "743", type: :bus, direction_names: ["Test Bus One", "Test Bus Two"]}]
- end)
- |> Enum.uniq()
- }
-
- %{stop_ids: stop_ids} ->
- {
- :ok,
- stop_ids
- |> Enum.flat_map(fn
- "place-knncl" ->
- [%{id: "Red", type: :subway}]
-
- "place-A" ->
- [%{id: "Orange", type: :subway}, %{id: "Green", type: :light_rail}]
-
- _ ->
- [%{id: "test", type: :test}]
- end)
- |> Enum.uniq()
- }
- end
-
- %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_alerts_fn: fetch_alerts_fn,
- fetch_schedules_fn: fetch_schedules_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- }
- end
-
- test "returns headway section when no departures, no alert and not in overnight period", %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_alerts_fn: fetch_alerts_fn,
- fetch_schedules_fn: fetch_schedules_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- } do
- config =
- put_primary_departures(config, [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-knncl"]}}}
- ])
-
- now = ~U[2020-04-06T10:00:00Z]
- expect(@headways, :get_with_route, fn "place-knncl", "Red", ^now -> {12, 16} end)
-
- expected_departures = [
- %DeparturesWidget{
- screen: config,
- sections: [
- %HeadwaySection{
- route: "Red",
- time_range: {12, 16},
- headsign: nil
- }
- ],
- slot_names: [:main_content_zero],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %HeadwaySection{
- route: "Red",
- time_range: {12, 16},
- headsign: nil
- }
- ],
- slot_names: [:main_content_one],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %HeadwaySection{
- route: "Red",
- time_range: {12, 16},
- headsign: nil
- }
- ],
- slot_names: [:main_content_two],
- now: now
- }
- ]
-
- actual_instances =
- Dup.Departures.departures_instances(
- config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
-
- assert Enum.all?(expected_departures, &Enum.member?(actual_instances, &1))
- end
-
- test "returns headway sections with direction names if the sections are configured with direction_id",
- %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_alerts_fn: fetch_alerts_fn,
- fetch_schedules_fn: fetch_schedules_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- } do
- config =
- put_primary_departures(config, [
- %Section{
- query: %Query{
- params: %Query.Params{stop_ids: ["place-aport"], route_ids: ["Blue"], direction_id: 0}
- }
- },
- %Section{
- query: %Query{
- params: %Query.Params{stop_ids: ["place-aport"], route_ids: ["743"], direction_id: 0}
- }
- }
- ])
-
- now = ~U[2020-04-06T10:00:00Z]
- expect(@headways, :get_with_route, fn "place-aport", "Blue", ^now -> {2, 4} end)
- expect(@headways, :get_with_route, fn "place-aport", "743", ^now -> {6, 8} end)
-
- expected_departures = [
- %DeparturesWidget{
- screen: config,
- sections: [
- %HeadwaySection{
- route: "Blue",
- time_range: {2, 4},
- headsign: "Test Direction Zero"
- },
- %HeadwaySection{
- route: "743",
- time_range: {6, 8},
- headsign: "Test Bus One"
- }
- ],
- slot_names: [:main_content_zero],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %HeadwaySection{
- route: "Blue",
- time_range: {2, 4},
- headsign: "Test Direction Zero"
- },
- %HeadwaySection{
- route: "743",
- time_range: {6, 8},
- headsign: "Test Bus One"
- }
- ],
- slot_names: [:main_content_one],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %HeadwaySection{
- route: "Blue",
- time_range: {2, 4},
- headsign: "Test Direction Zero"
- },
- %HeadwaySection{
- route: "743",
- time_range: {6, 8},
- headsign: "Test Bus One"
- }
- ],
- slot_names: [:main_content_two],
- now: now
- }
- ]
-
- actual_instances =
- Dup.Departures.departures_instances(
- config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
-
- assert Enum.all?(expected_departures, &Enum.member?(actual_instances, &1))
- end
-
- test "returns directional headway when at boundary for alerts", %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_schedules_fn: fetch_schedules_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- } do
- config =
- put_primary_departures(config, [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-B"]}}}
- ])
-
- now = ~U[2020-04-06T10:00:00Z]
- expect(@headways, :get_with_route, fn "place-B", "test", ^now -> {12, 16} end)
-
- fetch_alerts_fn = fn
- [
- direction_id: :both,
- route_ids: [],
- stop_ids: ["place-B"],
- route_types: [:light_rail, :subway]
- ] ->
- [
- struct(Alert,
- effect: :suspension,
- informed_entities: [
- ie(stop_id: "place-B", route: "Red")
- ],
- active_period: [{~U[2020-04-06T09:00:00Z], nil}]
- )
- ]
- end
-
- expected_departures = [
- %DeparturesWidget{
- screen: config,
- sections: [
- %HeadwaySection{
- route: "Red",
- time_range: {12, 16},
- headsign: "Test A"
- }
- ],
- slot_names: [:main_content_zero],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %HeadwaySection{
- route: "Red",
- time_range: {12, 16},
- headsign: "Test A"
- }
- ],
- slot_names: [:main_content_one],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %HeadwaySection{
- route: "Red",
- time_range: {12, 16},
- headsign: "Test A"
- }
- ],
- slot_names: [:main_content_two],
- now: now
- }
- ]
-
- actual_instances =
- Dup.Departures.departures_instances(
- config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
-
- assert Enum.all?(expected_departures, &Enum.member?(actual_instances, &1))
- end
-
- test "returns headway section and regular predictions when multiple departures configured but one section has headways",
- %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_alerts_fn: fetch_alerts_fn,
- fetch_schedules_fn: fetch_schedules_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- } do
- config =
- put_primary_departures(config, [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-A"]}}},
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-kencl"]}}}
- ])
-
- now = ~U[2020-04-06T10:00:00Z]
- expect(@headways, :get_with_route, fn "place-A", "Orange", ^now -> nil end)
- expect(@headways, :get_with_route, fn "place-A", "Green", ^now -> nil end)
- expect(@headways, :get_with_route, fn "place-kencl", "test", ^now -> {7, 13} end)
-
- expected_departures = [
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- },
- %HeadwaySection{
- route: "test",
- time_range: {7, 13},
- headsign: nil
- }
- ],
- slot_names: [:main_content_reduced_zero],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- },
- %HeadwaySection{
- route: "test",
- time_range: {7, 13},
- headsign: nil
- }
- ],
- slot_names: [:main_content_reduced_one],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- },
- %HeadwaySection{
- route: "test",
- time_range: {7, 13},
- headsign: nil
- }
- ],
- slot_names: [:main_content_reduced_two],
- now: now
- }
- ]
-
- actual_instances =
- Dup.Departures.departures_instances(
- config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
-
- assert Enum.all?(expected_departures, &Enum.member?(actual_instances, &1))
- end
-
- test "returns headway sections for branch station for alert with trunk headsign", %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_schedules_fn: fetch_schedules_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- } do
- config =
- put_primary_departures(config, [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-kencl"]}}}
- ])
-
- now = ~U[2020-04-06T10:00:00Z]
- expect(@headways, :get_with_route, fn "place-kencl", "test", ^now -> {7, 13} end)
-
- fetch_alerts_fn = fn
- [
- direction_id: :both,
- route_ids: [],
- stop_ids: ["place-kencl"],
- route_types: [:light_rail, :subway]
- ] ->
- [
- # Suspension alert from Kenmore to Hynes
- struct(Alert,
- effect: :suspension,
- informed_entities: [
- ie(stop_id: "place-kencl", route: "Green-C"),
- ie(stop_id: "place-hymnl", route: "Green-C"),
- ie(stop_id: "70151", route: "Green-C"),
- ie(stop_id: "70152", route: "Green-C")
- ],
- active_period: [{~U[2020-04-06T09:00:00Z], nil}]
- )
- ]
- end
-
- expected_departures = [
- %DeparturesWidget{
- screen: config,
- sections: [
- %HeadwaySection{
- route: "Green-C",
- time_range: {7, 13},
- headsign: "Westbound"
- }
- ],
- slot_names: [:main_content_reduced_zero],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %HeadwaySection{
- route: "Green-C",
- time_range: {7, 13},
- headsign: "Westbound"
- }
- ],
- slot_names: [:main_content_reduced_one],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %HeadwaySection{
- route: "Green-C",
- time_range: {7, 13},
- headsign: "Westbound"
- }
- ],
- slot_names: [:main_content_reduced_two],
- now: now
- }
- ]
-
- actual_instances =
- Dup.Departures.departures_instances(
- config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
-
- assert Enum.all?(expected_departures, &Enum.member?(actual_instances, &1))
- end
-end
diff --git a/test/screens/v2/candidate_generator/dup/departures_test.exs b/test/screens/v2/candidate_generator/dup/departures_test.exs
index f1501af16..4f06aee4c 100644
--- a/test/screens/v2/candidate_generator/dup/departures_test.exs
+++ b/test/screens/v2/candidate_generator/dup/departures_test.exs
@@ -1,7 +1,7 @@
defmodule Screens.V2.CandidateGenerator.Dup.DeparturesTest do
use ExUnit.Case, async: true
- alias Screens.Alerts.Alert
+ alias Screens.Lines.Line
alias Screens.Predictions.Prediction
alias Screens.Routes.Route
alias Screens.Schedules.Schedule
@@ -9,2887 +9,1308 @@ defmodule Screens.V2.CandidateGenerator.Dup.DeparturesTest do
alias Screens.Trips.Trip
alias Screens.V2.CandidateGenerator.Dup
alias Screens.V2.Departure
+ alias Screens.V2.RDS
alias Screens.V2.WidgetInstance.Departures, as: DeparturesWidget
- alias Screens.V2.WidgetInstance.Departures.{NoDataSection, NormalSection}
- alias Screens.V2.WidgetInstance.DeparturesNoData
+ alias Screens.V2.WidgetInstance.Departures.HeadwayRow
+ alias Screens.V2.WidgetInstance.{DeparturesNoData, DeparturesNoService}
alias Screens.V2.WidgetInstance.OvernightDepartures
- alias Screens.Vehicles.Vehicle
alias ScreensConfig.{Alerts, Departures, Header}
- alias ScreensConfig.Departures.Header, as: SectionHeader
- alias ScreensConfig.Departures.{Layout, Query, Section}
+ alias ScreensConfig.Departures.{Query, Section}
alias ScreensConfig.Screen
alias ScreensConfig.Screen.Dup, as: DupConfig
- import Screens.Inject
- import Screens.TestSupport.InformedEntityBuilder
import Mox
+ import Screens.Inject
+
setup :verify_on_exit!
- @headways injected(Screens.Headways)
+ @rds injected(RDS)
+
+ @now ~U[2020-04-06T10:00:00Z]
+ @config %Screen{
+ app_params: %DupConfig{
+ header: %Header.StopId{stop_id: "place-test"},
+ primary_departures: %Departures{
+ sections: []
+ },
+ secondary_departures: %Departures{
+ sections: []
+ },
+ alerts: struct(Alerts)
+ },
+ vendor: :outfront,
+ device_id: "TEST",
+ name: "TEST",
+ app_id: :dup_v2
+ }
+
+ defp rds_countdown(stop_id, line_id, headsign, expected_departures) do
+ %RDS{
+ stop: %Stop{id: stop_id},
+ line: %Line{id: line_id},
+ headsign: headsign,
+ state: %RDS.Countdowns{
+ departures: expected_departures
+ }
+ }
+ end
+
+ defp no_service(stop_id, line_id, headsign, routes \\ []) do
+ %RDS{
+ stop: %Stop{id: stop_id},
+ line: %Line{id: line_id},
+ headsign: headsign,
+ state: %RDS.NoService{routes: routes, direction_id: 0}
+ }
+ end
+
+ defp first_trip(stop_id, line_id, headsign, first_schedule) do
+ %RDS{
+ stop: %Stop{id: stop_id},
+ line: %Line{id: line_id},
+ headsign: headsign,
+ state: %RDS.FirstTrip{first_schedule: first_schedule}
+ }
+ end
+
+ defp service_ended(stop_id, line_id, headsign, last_schedule) do
+ %RDS{
+ stop: %Stop{id: stop_id},
+ line: %Line{id: line_id},
+ headsign: headsign,
+ state: %RDS.ServiceEnded{last_schedule: last_schedule}
+ }
+ end
+
+ defp headways(stop_id, line_id, headsign, route_id, direction_name, direction_id, range) do
+ %RDS{
+ stop: %Stop{id: stop_id},
+ line: %Line{id: line_id},
+ headsign: headsign,
+ state: %RDS.Headways{
+ route_id: route_id,
+ direction_name: direction_name,
+ direction_id: direction_id,
+ range: range
+ }
+ }
+ end
+
+ defp expected_departures_widget(
+ config,
+ expected_primary_sections,
+ expected_secondary_sections,
+ now \\ @now
+ ) do
+ [
+ %DeparturesWidget{
+ screen: config,
+ slot_names: [:main_content_zero],
+ now: now,
+ sections: expected_primary_sections
+ },
+ %DeparturesWidget{
+ screen: config,
+ slot_names: [:main_content_one],
+ now: now,
+ sections: expected_primary_sections
+ },
+ %DeparturesWidget{
+ screen: config,
+ slot_names: [:main_content_reduced_zero],
+ now: now,
+ sections: expected_primary_sections
+ },
+ %DeparturesWidget{
+ screen: config,
+ slot_names: [:main_content_reduced_one],
+ now: now,
+ sections: expected_primary_sections
+ },
+ %DeparturesWidget{
+ screen: config,
+ slot_names: [:main_content_two],
+ now: now,
+ sections: expected_secondary_sections
+ },
+ %DeparturesWidget{
+ screen: config,
+ slot_names: [:main_content_reduced_two],
+ now: now,
+ sections: expected_secondary_sections
+ }
+ ]
+ end
+
+ defp expected_overnight_departures_widget(config) do
+ [
+ %OvernightDepartures{screen: config, slot_names: [:main_content_zero]},
+ %OvernightDepartures{screen: config, slot_names: [:main_content_one]},
+ %OvernightDepartures{screen: config, slot_names: [:main_content_reduced_zero]},
+ %OvernightDepartures{screen: config, slot_names: [:main_content_reduced_one]},
+ %OvernightDepartures{screen: config, slot_names: [:main_content_two]},
+ %OvernightDepartures{screen: config, slot_names: [:main_content_reduced_two]}
+ ]
+ end
- defp put_primary_departures(widget, primary_departures_sections) do
+ defp put_primary_departures(config, primary_departures_sections) do
%{
- widget
+ config
| app_params: %{
- widget.app_params
+ config.app_params
| primary_departures: %Departures{sections: primary_departures_sections}
}
}
end
- defp put_secondary_departures_sections(widget, secondary_departures_sections) do
+ defp put_secondary_departures_sections(config, secondary_departures_sections) do
%{
- widget
+ config
| app_params: %{
- widget.app_params
+ config.app_params
| secondary_departures: %Departures{sections: secondary_departures_sections}
}
}
end
setup do
- stub(Screens.Headways.Mock, :get_with_route, fn _, _, _ -> nil end)
+ stub(@rds, :get, fn _, _ -> [{:ok, []}] end)
+ :ok
+ end
- config = %Screen{
- app_params: %DupConfig{
- header: %Header.StopId{stop_id: "place-test"},
- primary_departures: %Departures{
- sections: []
- },
- secondary_departures: %Departures{
- sections: []
- },
- alerts: struct(Alerts)
- },
- vendor: :outfront,
- device_id: "TEST",
- name: "TEST",
- app_id: :dup_v2
- }
+ describe "instances/3" do
+ test "returns DeparturesNoData on RDS returning errors" do
+ primary_departures = [
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["place-A"]}}},
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["place-B"]}}}
+ ]
- fetch_departures_fn = fn
- %{stop_ids: ["place-A"]}, _opts ->
- {:ok,
- [
- %Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- )
- }
- ]}
-
- %{stop_ids: ["place-B"]}, _opts ->
- {:ok,
- [
- %Departure{
- prediction:
- struct(Prediction,
- id: "B1",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- )
- },
- %Departure{
- prediction:
- struct(Prediction,
- id: "B2",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- )
- },
- %Departure{
- prediction:
- struct(Prediction,
- id: "B3",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- )
- },
- %Departure{
- prediction:
- struct(Prediction,
- id: "B4",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- )
- },
- %Departure{
- prediction:
- struct(Prediction,
- id: "B5",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- )
- }
- ]}
-
- %{stop_ids: ["place-C"]}, _opts ->
- {:ok,
- [
- %Departure{
- prediction:
- struct(Prediction,
- id: "C",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- )
- }
- ]}
-
- %{stop_ids: ["place-D"]}, _opts ->
- {:ok,
- [
- %Departure{
- prediction:
- struct(Prediction,
- id: "D",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- )
- }
- ]}
-
- %{stop_ids: ["place-F"]}, _opts ->
- {:ok,
- [
- %Departure{
- prediction: %Prediction{
- id: "F1",
- trip: %Trip{direction_id: 0},
- stop: struct(Stop),
- route: %Route{id: "Test"}
- }
- },
- %Departure{
- prediction: %Prediction{
- id: "F2",
- trip: %Trip{direction_id: 0},
- stop: struct(Stop),
- route: %Route{id: "Test"}
- }
- },
- %Departure{
- prediction: %Prediction{
- id: "F3",
- trip: %Trip{direction_id: 0},
- stop: struct(Stop),
- route: %Route{id: "Test"}
- }
- },
- %Departure{
- prediction: %Prediction{
- id: "F4",
- trip: %Trip{direction_id: 1},
- stop: struct(Stop),
- route: %Route{id: "Test"}
- }
- },
- %Departure{
- prediction: %Prediction{
- id: "F5",
- trip: %Trip{direction_id: 1},
- stop: struct(Stop),
- route: %Route{id: "Test"}
- }
- }
- ]}
-
- %{stop_ids: ["place-G"]}, _opts ->
- {:ok,
- [
- %Departure{
- prediction: %Prediction{
- id: "G1",
- trip: %Trip{direction_id: 1},
- stop: struct(Stop),
- route: %Route{id: "Test"}
- }
- }
- ]}
-
- %{stop_ids: ["place-kencl"]}, _opts ->
- {:ok,
- [
- %Departure{
- prediction:
- struct(Prediction,
- id: "Kenmore",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- )
- }
- ]}
-
- %{stop_ids: ["bus-A", "bus-B"]}, _opts ->
- {:ok,
- [
- %Departure{
- prediction:
- struct(Prediction,
- id: "Bus A",
- route: %Route{id: "Bus A", type: :bus},
- stop: struct(Stop),
- trip: struct(Trip)
- )
- }
- ]}
-
- _, _ ->
- {:ok, []}
- end
+ secondary_departures = [
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["place-C"]}}},
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["place-D"]}}}
+ ]
- fetch_alerts_fn = fn
- _ -> []
- end
+ config =
+ @config
+ |> put_primary_departures(primary_departures)
+ |> put_secondary_departures_sections(secondary_departures)
- fetch_schedules_fn = fn
- _, _ ->
- []
- end
+ expect(@rds, :get, fn _primary_departures, @now -> [:error, :error] end)
+ expect(@rds, :get, fn _secondary_departures, @now -> [:error, :error] end)
- fetch_vehicles_fn = fn _, _ -> [struct(Vehicle)] end
-
- fetch_routes_fn = fn
- %{ids: ids} ->
- {
- :ok,
- ids
- |> Enum.flat_map(fn
- "Ferry" -> [%{id: "Ferry", type: :ferry}]
- "Orange" -> [%{id: "Orange", type: :subway}]
- "Green" -> [%{id: "Green", type: :light_rail}]
- "Bus A" -> [%{id: "Bus A", type: :bus}]
- "Bus B" -> [%{id: "Bus B", type: :bus}]
- "Bus C" -> [%{id: "Bus C", type: :bus}]
- "Red" -> [%{id: "Red", type: :subway}]
- end)
- |> Enum.uniq()
- }
+ expected_instances = [
+ %DeparturesNoData{screen: config, slot_name: :main_content_zero},
+ %DeparturesNoData{screen: config, slot_name: :main_content_one},
+ %DeparturesNoData{screen: config, slot_name: :main_content_reduced_zero},
+ %DeparturesNoData{screen: config, slot_name: :main_content_reduced_one},
+ %DeparturesNoData{screen: config, slot_name: :main_content_two},
+ %DeparturesNoData{screen: config, slot_name: :main_content_reduced_two}
+ ]
- %{stop_ids: stop_ids} ->
- {
- :ok,
- stop_ids
- |> Enum.flat_map(fn
- "Boat" -> [%{id: "Ferry", type: :ferry}]
- "place-A" -> [%{id: "Orange", type: :subway}, %{id: "Green", type: :light_rail}]
- "bus-A" -> [%{id: "Bus A", type: :bus}]
- "bus-B" -> [%{id: "Bus B", type: :bus}]
- "bus-C" -> [%{id: "Bus C", type: :bus}]
- "bus-C+D" -> [%{id: "Bus C", type: :bus}, %{id: "Bus D", type: :bus}]
- "place-overnight" -> [%{id: "Red", type: :subway}]
- "place-closed" -> [%{id: "Red", type: :subway}, %{id: "Bus A", type: :bus}]
- _ -> [%{id: "test", type: :test}]
- end)
- |> Enum.uniq()
- }
+ actual_instances = Dup.Departures.instances(config, @now)
+
+ assert actual_instances == expected_instances
end
- %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_alerts_fn: fetch_alerts_fn,
- fetch_schedules_fn: fetch_schedules_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- }
- end
+ test "returns DeparturesNoService on RDS returning NoService states" do
+ primary_departures = [
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["place-A"]}}},
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["place-B"]}}}
+ ]
+
+ secondary_departures = [
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["place-C"]}}},
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["place-D"]}}}
+ ]
- describe "departures_instances/4" do
- test "returns primary and secondary departures", %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_alerts_fn: fetch_alerts_fn,
- fetch_schedules_fn: fetch_schedules_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- } do
config =
- config
- |> put_primary_departures([
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-A"]}}},
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-B"]}}}
- ])
- |> put_secondary_departures_sections([
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-C"]}}},
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-D"]}}}
- ])
-
- now = ~U[2020-04-06T10:00:00Z]
+ @config
+ |> put_primary_departures(primary_departures)
+ |> put_secondary_departures_sections(secondary_departures)
- expected_departures = [
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- },
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B1",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B2",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_zero],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- },
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B1",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B2",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_one],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "C",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- },
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "D",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_two],
- now: now
- }
+ expect(@rds, :get, fn _primary_departures, @now ->
+ [
+ {:ok, [no_service("place-A", "line-A", "headsign")]},
+ {:ok, [no_service("place-B", "line-B", "headsign2")]}
+ ]
+ end)
+
+ expect(@rds, :get, fn _secondary_departures, @now -> [] end)
+
+ expected_instances = [
+ %DeparturesNoService{screen: config, slot_name: :main_content_zero},
+ %DeparturesNoService{screen: config, slot_name: :main_content_one},
+ %DeparturesNoService{screen: config, slot_name: :main_content_reduced_zero},
+ %DeparturesNoService{screen: config, slot_name: :main_content_reduced_one},
+ %DeparturesNoService{screen: config, slot_name: :main_content_two},
+ %DeparturesNoService{screen: config, slot_name: :main_content_reduced_two}
]
- actual_instances =
- Dup.Departures.departures_instances(
- config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
+ actual_instances = Dup.Departures.instances(config, @now)
- assert Enum.all?(expected_departures, &Enum.member?(actual_instances, &1))
+ assert actual_instances == expected_instances
end
- test "returns only primary departures if secondary is missing", %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_alerts_fn: fetch_alerts_fn,
- fetch_schedules_fn: fetch_schedules_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- } do
- config =
- put_primary_departures(config, [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-A"]}}},
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-B"]}}}
- ])
+ test "secondary departures fallback to primary departures when empty" do
+ primary_departures = [
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s2"]}}}
+ ]
- now = ~U[2020-04-06T10:00:00Z]
+ secondary_departures = []
expected_departures = [
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- },
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B1",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B2",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_zero],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- },
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B1",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B2",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_one],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- },
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B1",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B2",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_two],
- now: now
+ %Departure{
+ prediction: %Prediction{
+ arrival_time: ~U[2024-10-11 12:27:00Z],
+ departure_time: ~U[2024-10-11 12:30:00Z],
+ route: %Route{id: "r1", line: %Line{id: "l1"}, type: :bus},
+ stop: %Stop{id: "s1"},
+ trip: %Trip{headsign: "other1", pattern_headsign: "h1"}
+ },
+ schedule: nil
}
]
- actual_instances =
- Dup.Departures.departures_instances(
- config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
-
- assert Enum.all?(expected_departures, &Enum.member?(actual_instances, &1))
- end
+ expected_primary_sections = [
+ %Screens.V2.WidgetInstance.Departures.NormalSection{
+ header: %ScreensConfig.Departures.Header{
+ arrow: nil,
+ read_as: nil,
+ subtitle: nil,
+ title: nil
+ },
+ layout: %ScreensConfig.Departures.Layout{
+ base: nil,
+ include_later: false,
+ max: nil,
+ min: 1
+ },
+ grouping_type: :time,
+ rows: expected_departures
+ }
+ ]
- test "returns only primary departures if secondary has no data", %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_alerts_fn: fetch_alerts_fn,
- fetch_schedules_fn: fetch_schedules_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- } do
config =
- config
- |> put_primary_departures([
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-A"]}}}
- ])
- |> put_secondary_departures_sections([
- %Section{query: %Query{params: %Query.Params{stop_ids: ["nonexist"]}}}
- ])
-
- now = ~U[2020-04-06T10:00:00Z]
-
- primary_section = %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
+ @config
+ |> put_primary_departures(primary_departures)
+ |> put_secondary_departures_sections(secondary_departures)
+
+ expect(@rds, :get, fn _primary_departures, @now ->
+ [
+ {:ok, [rds_countdown("s1", "l1", "other1", expected_departures)]}
]
- }
+ end)
- expected_departures = [
- %DeparturesWidget{
- screen: config,
- sections: [primary_section],
- slot_names: [:main_content_zero],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [primary_section],
- slot_names: [:main_content_one],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [primary_section],
- slot_names: [:main_content_two],
- now: now
- }
- ]
+ expect(@rds, :get, fn _secondary_departures, @now -> [{:ok, []}, {:ok, []}] end)
- actual_instances =
- Dup.Departures.departures_instances(
- config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
+ expected_instances =
+ expected_departures_widget(config, expected_primary_sections, expected_primary_sections)
+
+ actual_instances = Dup.Departures.instances(config, @now)
- assert Enum.all?(expected_departures, &Enum.member?(actual_instances, &1))
+ assert actual_instances == expected_instances
end
- test "returns only bidirectional departures if configured for that", %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_alerts_fn: fetch_alerts_fn,
- fetch_schedules_fn: fetch_schedules_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- } do
- config =
- put_primary_departures(config, [
- %Section{
- bidirectional: true,
- query: %Query{params: %Query.Params{stop_ids: ["place-F"]}}
- },
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-A"]}}}
- ])
+ test "creates no service sections for no service states" do
+ primary_departures = [
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s2"]}}}
+ ]
- now = ~U[2020-04-06T10:00:00Z]
+ secondary_departures = []
expected_departures = [
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "F1",
- trip: struct(Trip, direction_id: 0),
- stop: struct(Stop),
- route: %Route{id: "Test"}
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "F4",
- trip: struct(Trip, direction_id: 1),
- stop: struct(Stop),
- route: %Route{id: "Test"}
- ),
- schedule: nil
- }
- ]
- },
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- stop: struct(Stop),
- route: %Route{id: "Test"},
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_zero],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "F1",
- trip: struct(Trip, direction_id: 0),
- stop: struct(Stop),
- route: %Route{id: "Test"}
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "F4",
- trip: struct(Trip, direction_id: 1),
- stop: struct(Stop),
- route: %Route{id: "Test"}
- ),
- schedule: nil
- }
- ]
- },
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- stop: struct(Stop),
- route: %Route{id: "Test"},
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_one],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "F1",
- trip: struct(Trip, direction_id: 0),
- stop: struct(Stop),
- route: %Route{id: "Test"}
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "F4",
- trip: struct(Trip, direction_id: 1),
- stop: struct(Stop),
- route: %Route{id: "Test"}
- ),
- schedule: nil
- }
- ]
- },
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- stop: struct(Stop),
- route: %Route{id: "Test"},
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_two],
- now: now
+ %Departure{
+ prediction: %Prediction{
+ arrival_time: ~U[2024-10-11 12:27:00Z],
+ departure_time: ~U[2024-10-11 12:30:00Z],
+ route: %Route{id: "r1", line: %Line{id: "l1"}, type: :bus},
+ stop: %Stop{id: "s1"},
+ trip: %Trip{headsign: "other1", pattern_headsign: "h1"}
+ },
+ schedule: nil
}
]
- actual_instances =
- Dup.Departures.departures_instances(
- config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
-
- assert Enum.all?(expected_departures, &Enum.member?(actual_instances, &1))
- end
+ expected_primary_sections = [
+ %Screens.V2.WidgetInstance.Departures.NormalSection{
+ header: %ScreensConfig.Departures.Header{
+ arrow: nil,
+ read_as: nil,
+ subtitle: nil,
+ title: nil
+ },
+ layout: %ScreensConfig.Departures.Layout{
+ base: nil,
+ include_later: false,
+ max: nil,
+ min: 1
+ },
+ grouping_type: :time,
+ rows: expected_departures
+ },
+ %Screens.V2.WidgetInstance.Departures.NoServiceSection{routes: []}
+ ]
- test "returns one row for bidirectional departures if only one departure exists", %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_alerts_fn: fetch_alerts_fn,
- fetch_schedules_fn: fetch_schedules_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- } do
config =
- put_primary_departures(config, [
- %Section{
- bidirectional: true,
- query: %Query{params: %Query.Params{stop_ids: ["place-G"]}}
- }
- ])
+ @config
+ |> put_primary_departures(primary_departures)
+ |> put_secondary_departures_sections(secondary_departures)
- now = ~U[2020-04-06T10:00:00Z]
+ expect(@rds, :get, fn _primary_departures, @now ->
+ [
+ {:ok,
+ [
+ rds_countdown("s1", "l1", "other1", expected_departures)
+ ]},
+ {:ok, [no_service("s2", "l2", "other2")]}
+ ]
+ end)
- expected_departures = [
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "G1",
- stop: struct(Stop),
- route: %Route{id: "Test"},
- trip: struct(Trip, direction_id: 1)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_zero],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "G1",
- stop: struct(Stop),
- route: %Route{id: "Test"},
- trip: struct(Trip, direction_id: 1)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_one],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "G1",
- stop: struct(Stop),
- route: %Route{id: "Test"},
- trip: struct(Trip, direction_id: 1)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_two],
- now: now
- }
- ]
+ expect(@rds, :get, fn _secondary_departures, @now -> [{:ok, []}, {:ok, []}] end)
- actual_instances =
- Dup.Departures.departures_instances(
- config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
+ expected_instances =
+ expected_departures_widget(config, expected_primary_sections, expected_primary_sections)
- assert Enum.all?(expected_departures, &Enum.member?(actual_instances, &1))
+ actual_instances = Dup.Departures.instances(config, @now)
+
+ assert actual_instances == expected_instances
end
- test "returns 4 departures if only one section", %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_alerts_fn: fetch_alerts_fn,
- fetch_schedules_fn: fetch_schedules_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- } do
- config =
- put_primary_departures(config, [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-B"]}}}
- ])
+ test "creates no service section with routes for no service states that have routes" do
+ primary_departures = [
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s2"]}}}
+ ]
+
+ expected_route = %Route{id: "r1", line: %Line{id: "l1"}, type: :bus}
- now = ~U[2020-04-06T10:00:00Z]
+ secondary_departures = []
expected_departures = [
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B1",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B2",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B3",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B4",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_zero],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B1",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B2",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B3",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B4",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_one],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B1",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B2",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B3",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B4",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_two],
- now: now
+ %Departure{
+ prediction: %Prediction{
+ arrival_time: ~U[2024-10-11 12:27:00Z],
+ departure_time: ~U[2024-10-11 12:30:00Z],
+ route: expected_route,
+ stop: %Stop{id: "s1"},
+ trip: %Trip{headsign: "other1", pattern_headsign: "h1"}
+ },
+ schedule: nil
}
]
- actual_instances =
- Dup.Departures.departures_instances(
- config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
-
- assert Enum.all?(expected_departures, &Enum.member?(actual_instances, &1))
- end
+ expected_primary_sections = [
+ %Screens.V2.WidgetInstance.Departures.NormalSection{
+ header: %ScreensConfig.Departures.Header{
+ arrow: nil,
+ read_as: nil,
+ subtitle: nil,
+ title: nil
+ },
+ layout: %ScreensConfig.Departures.Layout{
+ base: nil,
+ include_later: false,
+ max: nil,
+ min: 1
+ },
+ grouping_type: :time,
+ rows: expected_departures
+ },
+ %Screens.V2.WidgetInstance.Departures.NoServiceSection{routes: [expected_route]}
+ ]
- test "returns normal sections for upcoming alert", %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_schedules_fn: fetch_schedules_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- } do
config =
- put_primary_departures(config, [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-B"]}}}
- ])
-
- now = ~U[2020-04-06T10:00:00Z]
+ @config
+ |> put_primary_departures(primary_departures)
+ |> put_secondary_departures_sections(secondary_departures)
- fetch_alerts_fn = fn
+ expect(@rds, :get, fn _primary_departures, @now ->
[
- direction_id: :both,
- route_ids: [],
- stop_ids: ["place-B"],
- route_types: [:light_rail, :subway]
- ] ->
- [
- struct(Alert,
- effect: :suspension,
- informed_entities: [
- ie(stop_id: "place-B", route: "Red"),
- ie(stop_id: "place-C", route: "Red")
- ],
- active_period: [{~U[2020-05-06T09:00:00Z], nil}]
- )
- ]
- end
+ {:ok, [rds_countdown("s1", "l1", "other1", expected_departures)]},
+ {:ok, [no_service("s2", "l2", "other2", [expected_route])]}
+ ]
+ end)
- expected_departures = [
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B1",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B2",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B3",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B4",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_zero],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B1",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B2",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B3",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B4",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_one],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B1",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B2",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B3",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "B4",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_two],
- now: now
- }
- ]
+ expect(@rds, :get, fn _secondary_departures, @now -> [{:ok, []}, {:ok, []}] end)
- actual_instances =
- Dup.Departures.departures_instances(
- config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
+ expected_instances =
+ expected_departures_widget(config, expected_primary_sections, expected_primary_sections)
+
+ actual_instances = Dup.Departures.instances(config, @now)
- assert Enum.all?(expected_departures, &Enum.member?(actual_instances, &1))
+ assert actual_instances == expected_instances
end
- test "returns normal sections for branch station for alert with branch terminal headsign", %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_schedules_fn: fetch_schedules_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- } do
- config =
- put_primary_departures(config, [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-kencl"]}}}
- ])
+ test "creates NormalSections for upcoming predictions and schedules" do
+ primary_departures = [
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s2"]}}}
+ ]
- now = ~U[2020-04-06T10:00:00Z]
+ secondary_departures = [
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s3"]}}},
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s4"]}}}
+ ]
- fetch_alerts_fn = fn
- [
- direction_id: :both,
- route_ids: [],
- stop_ids: ["place-kencl"],
- route_types: [:light_rail, :subway]
- ] ->
- [
- # Suspension alert from Kenmore to Saint Mary's Street
- struct(Alert,
- effect: :suspension,
- informed_entities: [
- ie(stop_id: "place-kencl", route: "Green-C"),
- ie(stop_id: "place-smary", route: "Green-C"),
- ie(stop_id: "70150", route: "Green-C"),
- ie(stop_id: "70151", route: "Green-C"),
- ie(stop_id: "70211", route: "Green-C"),
- ie(stop_id: "70212", route: "Green-C")
- ],
- active_period: [{~U[2020-04-06T09:00:00Z], nil}]
- )
- ]
- end
+ expected_primary_departures = [
+ %Departure{
+ prediction: %Prediction{
+ arrival_time: ~U[2024-10-11 12:27:00Z],
+ departure_time: ~U[2024-10-11 12:30:00Z],
+ route: %Route{id: "r1", line: %Line{id: "l1"}, type: :bus},
+ stop: %Stop{id: "s1"},
+ trip: %Trip{headsign: "other1", pattern_headsign: "h1"}
+ },
+ schedule: nil
+ }
+ ]
- expected_departures = [
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "Kenmore",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_zero],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "Kenmore",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_one],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "Kenmore",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_two],
- now: now
+ expected_secondary_departures = [
+ %Departure{
+ prediction: nil,
+ schedule: %Schedule{
+ departure_time: ~U[2024-10-11 13:15:00Z],
+ route: %Route{id: "r3", line: %Line{id: "l3"}, type: :ferry},
+ stop: %Stop{id: "s3"},
+ trip: %Trip{headsign: "other3", pattern_headsign: "h3"}
+ }
}
]
- actual_instances =
- Dup.Departures.departures_instances(
- config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
+ expected_primary_sections = [
+ %Screens.V2.WidgetInstance.Departures.NormalSection{
+ header: %ScreensConfig.Departures.Header{
+ arrow: nil,
+ read_as: nil,
+ subtitle: nil,
+ title: nil
+ },
+ layout: %ScreensConfig.Departures.Layout{
+ base: nil,
+ include_later: false,
+ max: nil,
+ min: 1
+ },
+ grouping_type: :time,
+ rows: expected_primary_departures
+ }
+ ]
- assert Enum.all?(expected_departures, &Enum.member?(actual_instances, &1))
- end
+ expected_secondary_sections = [
+ %Screens.V2.WidgetInstance.Departures.NormalSection{
+ header: %ScreensConfig.Departures.Header{
+ arrow: nil,
+ read_as: nil,
+ subtitle: nil,
+ title: nil
+ },
+ layout: %ScreensConfig.Departures.Layout{
+ base: nil,
+ include_later: false,
+ max: nil,
+ min: 1
+ },
+ grouping_type: :time,
+ rows: expected_secondary_departures
+ }
+ ]
- test "returns no data sections for disabled mode", %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_alerts_fn: fetch_alerts_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_schedules_fn: fetch_schedules_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- } do
config =
- config
- |> put_primary_departures([
- %Section{query: %Query{params: %Query.Params{stop_ids: ["Boat"]}}},
- %Section{
- query: %Query{params: %Query.Params{stop_ids: ["place-A"], route_ids: ["Orange"]}}
- }
- ])
- |> put_secondary_departures_sections([
- %Section{
- query: %Query{params: %Query.Params{stop_ids: ["place-A"], route_ids: ["Orange"]}}
- },
- %Section{
- query: %Query{params: %Query.Params{stop_ids: ["place-A"], route_ids: ["Green"]}}
- }
- ])
+ @config
+ |> put_primary_departures(primary_departures)
+ |> put_secondary_departures_sections(secondary_departures)
+
+ expect(@rds, :get, fn _primary_departures, @now ->
+ [{:ok, [rds_countdown("s1", "l1", "other1", expected_primary_departures)]}]
+ end)
+
+ expect(@rds, :get, fn _secondary_departures, @now ->
+ [{:ok, [rds_countdown("s3", "l3", "other3", expected_secondary_departures)]}]
+ end)
+
+ expected_instances =
+ expected_departures_widget(config, expected_primary_sections, expected_secondary_sections)
+
+ actual_instances = Dup.Departures.instances(config, @now)
- now = ~U[2020-04-06T10:00:00Z]
+ assert actual_instances == expected_instances
+ end
+
+ test "handles bidirectional flag while creating departure sections" do
+ primary_departures = [
+ %Section{bidirectional: true, query: %Query{params: %Query.Params{stop_ids: ["s1"]}}}
+ ]
expected_departures = [
- %DeparturesWidget{
- screen: config,
- sections: [
- %NoDataSection{route: %{id: "Ferry", type: :ferry}},
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_zero],
- now: now
+ %Departure{
+ prediction: %Prediction{
+ arrival_time: ~U[2024-10-11 12:27:00Z],
+ departure_time: ~U[2024-10-11 12:30:00Z],
+ route: %Route{id: "r1", line: %Line{id: "l1"}, type: :bus},
+ stop: %Stop{id: "s1"},
+ trip: %Trip{headsign: "other1", pattern_headsign: "h1", direction_id: 0}
+ },
+ schedule: nil
},
- %DeparturesWidget{
- screen: config,
- sections: [
- %NoDataSection{route: %{id: "Ferry", type: :ferry}},
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_one],
- now: now
+ %Departure{
+ prediction: %Prediction{
+ arrival_time: ~U[2024-10-11 12:30:00Z],
+ departure_time: ~U[2024-10-11 12:32:00Z],
+ route: %Route{id: "r1", line: %Line{id: "l1"}, type: :bus},
+ stop: %Stop{id: "s1"},
+ trip: %Trip{headsign: "other3", pattern_headsign: "h3", direction_id: 1}
+ },
+ schedule: nil
+ }
+ ]
+
+ all_departures = [
+ %Departure{
+ prediction: %Prediction{
+ arrival_time: ~U[2024-10-11 12:27:00Z],
+ departure_time: ~U[2024-10-11 12:30:00Z],
+ route: %Route{id: "r1", line: %Line{id: "l1"}, type: :bus},
+ stop: %Stop{id: "s1"},
+ trip: %Trip{headsign: "other1", pattern_headsign: "h1", direction_id: 0}
+ },
+ schedule: nil
+ },
+ %Departure{
+ prediction: %Prediction{
+ arrival_time: ~U[2024-10-11 12:27:00Z],
+ departure_time: ~U[2024-10-11 12:30:00Z],
+ route: %Route{id: "r1", line: %Line{id: "l1"}, type: :bus},
+ stop: %Stop{id: "s1"},
+ trip: %Trip{headsign: "other1", pattern_headsign: "h1", direction_id: 0}
+ },
+ schedule: nil
+ },
+ %Departure{
+ prediction: %Prediction{
+ arrival_time: ~U[2024-10-11 12:27:00Z],
+ departure_time: ~U[2024-10-11 12:30:00Z],
+ route: %Route{id: "r1", line: %Line{id: "l1"}, type: :bus},
+ stop: %Stop{id: "s1"},
+ trip: %Trip{headsign: "other1", pattern_headsign: "h1", direction_id: 0}
+ },
+ schedule: nil
},
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- },
- %NoDataSection{route: %{id: "Green", type: :light_rail}}
- ],
- slot_names: [:main_content_two],
- now: now
+ %Departure{
+ prediction: %Prediction{
+ arrival_time: ~U[2024-10-11 12:30:00Z],
+ departure_time: ~U[2024-10-11 12:32:00Z],
+ route: %Route{id: "r1", line: %Line{id: "l1"}, type: :bus},
+ stop: %Stop{id: "s1"},
+ trip: %Trip{headsign: "other3", pattern_headsign: "h3", direction_id: 1}
+ },
+ schedule: nil
}
]
- actual_instances =
- Dup.Departures.departures_instances(
- config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
+ expected_sections = [
+ %Screens.V2.WidgetInstance.Departures.NormalSection{
+ header: %ScreensConfig.Departures.Header{
+ arrow: nil,
+ read_as: nil,
+ subtitle: nil,
+ title: nil
+ },
+ layout: %ScreensConfig.Departures.Layout{
+ base: nil,
+ include_later: false,
+ max: nil,
+ min: 1
+ },
+ grouping_type: :time,
+ rows: expected_departures
+ }
+ ]
- assert Enum.all?(expected_departures, &Enum.member?(actual_instances, &1))
- end
+ config =
+ @config
+ |> put_primary_departures(primary_departures)
- test "consolidates into DeparturesNoData only when all rotations have no data", %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_alerts_fn: fetch_alerts_fn,
- fetch_schedules_fn: fetch_schedules_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- } do
- now = ~U[2020-04-06T10:00:00Z]
-
- instances_partial_data =
- config
- |> put_primary_departures([
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-A"], route_ids: []}}}
- ])
- |> put_secondary_departures_sections([
- %Section{query: %Query{params: %Query.Params{stop_ids: ["nonexist"], route_ids: []}}}
- ])
- |> Dup.Departures.departures_instances(
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
+ expect(@rds, :get, fn _primary_departures, @now ->
+ [{:ok, [rds_countdown("s1", "l1", "other1", all_departures)]}]
+ end)
- instances_no_data =
- config
- |> put_primary_departures([
- %Section{query: %Query{params: %Query.Params{stop_ids: ["nonexist1"], route_ids: []}}}
- ])
- |> put_secondary_departures_sections([
- %Section{query: %Query{params: %Query.Params{stop_ids: ["nonexist2"], route_ids: []}}}
- ])
- |> Dup.Departures.departures_instances(
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
+ expect(@rds, :get, fn _secondary_departures, @now ->
+ [{:ok, []}]
+ end)
+
+ expected_instances =
+ expected_departures_widget(config, expected_sections, expected_sections)
- assert Enum.all?(instances_partial_data, &match?(%DeparturesWidget{}, &1))
- assert Enum.all?(instances_no_data, &match?(%DeparturesNoData{}, &1))
+ actual_instances = Dup.Departures.instances(config, @now)
+
+ assert actual_instances == expected_instances
end
- end
- describe "overnight mode" do
- test "returns normal sections with normal rows and overnight rows for routes in overnight mode",
- %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_alerts_fn: fetch_alerts_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- } do
- config =
- config
- |> put_primary_departures([
- %Section{
- query: %Query{params: %Query.Params{stop_ids: ["place-A"]}}
- }
- ])
- |> put_secondary_departures_sections([
- %Section{
- query: %Query{params: %Query.Params{stop_ids: ["bus-A", "bus-B"]}}
- }
- ])
+ test "creates NormalSections with First Departure rows for early morning scheduled departures" do
+ primary_departures = [
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s2"]}}}
+ ]
- now = ~U[2020-04-06T10:00:00Z]
+ secondary_departures = [
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s3"]}}},
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s4"]}}}
+ ]
- fetch_schedules_fn = fn
- _, nil ->
- {:ok,
- [
- %Schedule{
- departure_time: ~U[2020-04-06T09:00:00Z],
- route: %Route{id: "Bus B"},
- stop: struct(Stop, id: "bus-B")
- }
- ]}
+ expected_primary_schedule =
+ %Schedule{
+ arrival_time: ~U[2024-10-11 10:27:00Z],
+ departure_time: ~U[2024-10-11 10:30:00Z],
+ route: %Route{id: "r1", line: %Line{id: "l1"}, type: :subway},
+ stop: %Stop{id: "s1"},
+ trip: %Trip{headsign: "other1", pattern_headsign: "h1"}
+ }
- _, _ ->
- {:ok,
- [
- %Schedule{
- departure_time: ~U[2020-04-07T09:00:00Z],
- route: %Route{id: "Bus B"},
- stop: struct(Stop, id: "bus-B")
- }
- ]}
- end
+ expected_secondary_schedule =
+ %Schedule{
+ departure_time: ~U[2024-10-11 10:15:00Z],
+ route: %Route{id: "r3", line: %Line{id: "l3"}, type: :subway},
+ stop: %Stop{id: "s3"},
+ trip: %Trip{headsign: "other3", pattern_headsign: "h3"}
+ }
- expected_departures = [
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_zero],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_one],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "Bus A",
- route: %Route{id: "Bus A", type: :bus},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction: nil,
- schedule:
- struct(Schedule,
- departure_time: ~U[2020-04-07T09:00:00Z],
- route: %Route{id: "Bus B"},
- stop: struct(Stop, id: "bus-B")
- )
- }
- ]
- }
- ],
- slot_names: [:main_content_two],
- now: now
+ expected_primary_departures = [{expected_primary_schedule, :first_trip}]
+ expected_secondary_departures = [{expected_secondary_schedule, :first_trip}]
+
+ expected_primary_sections = [
+ %Screens.V2.WidgetInstance.Departures.NormalSection{
+ header: %ScreensConfig.Departures.Header{
+ arrow: nil,
+ read_as: nil,
+ subtitle: nil,
+ title: nil
+ },
+ layout: %ScreensConfig.Departures.Layout{
+ base: nil,
+ include_later: false,
+ max: nil,
+ min: 1
+ },
+ grouping_type: :time,
+ rows: expected_primary_departures
}
]
- actual_instances =
- Dup.Departures.departures_instances(
- config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
-
- assert Enum.all?(expected_departures, &Enum.member?(actual_instances, &1))
- end
+ expected_secondary_sections = [
+ %Screens.V2.WidgetInstance.Departures.NormalSection{
+ header: %ScreensConfig.Departures.Header{
+ arrow: nil,
+ read_as: nil,
+ subtitle: nil,
+ title: nil
+ },
+ layout: %ScreensConfig.Departures.Layout{
+ base: nil,
+ include_later: false,
+ max: nil,
+ min: 1
+ },
+ grouping_type: :time,
+ rows: expected_secondary_departures
+ }
+ ]
- test "returns normal sections with normal rows and overnight rows with nil scheduled times for routes in overnight mode with no scheduled trips tomorrow",
- %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_alerts_fn: fetch_alerts_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- } do
config =
- config
- |> put_primary_departures([
- %Section{
- query: %Query{params: %Query.Params{stop_ids: ["place-A"]}}
- }
- ])
- |> put_secondary_departures_sections([
- %Section{
- query: %Query{params: %Query.Params{stop_ids: ["bus-A", "bus-B"]}}
- }
- ])
+ @config
+ |> put_primary_departures(primary_departures)
+ |> put_secondary_departures_sections(secondary_departures)
- now = ~U[2020-04-06T10:00:00Z]
+ expect(@rds, :get, fn _primary_departures, @now ->
+ [{:ok, [first_trip("s1", "l1", "other1", expected_primary_schedule)]}]
+ end)
- fetch_schedules_fn = fn
- _, ^now ->
- {:ok,
- [
- %Schedule{
- departure_time: ~U[2020-04-06T09:00:00Z],
- route: %Route{id: "Bus B"},
- stop: struct(Stop, id: "bus-B")
- }
- ]}
+ expect(@rds, :get, fn _secondary_departures, @now ->
+ [{:ok, [first_trip("s3", "l3", "other3", expected_secondary_schedule)]}]
+ end)
- _, _ ->
- {:ok, []}
- end
+ expected_instances =
+ expected_departures_widget(config, expected_primary_sections, expected_secondary_sections)
- expected_departures = [
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_zero],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_one],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "Bus A",
- route: %Route{id: "Bus A", type: :bus},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- },
- %Screens.V2.Departure{
- prediction: nil,
- schedule:
- struct(Schedule,
- departure_time: nil,
- route: %Route{id: "Bus B"},
- stop: struct(Stop, id: "bus-B")
- )
- }
- ]
- }
- ],
- slot_names: [:main_content_two],
- now: now
- }
- ]
-
- actual_instances =
- Dup.Departures.departures_instances(
- config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
+ actual_instances = Dup.Departures.instances(config, @now)
- assert Enum.all?(expected_departures, &Enum.member?(actual_instances, &1))
+ assert actual_instances == expected_instances
end
- @tag capture_log: true
- test "returns no-data if now is after tomorrow's first schedule",
- %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_alerts_fn: fetch_alerts_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- } do
- config =
- config
- |> put_primary_departures([
- %Section{
- query: %Query{params: %Query.Params{stop_ids: ["bus-B"]}}
- }
- ])
+ test "creates NormalSections with Overnight Departure rows for service ended destinations" do
+ now = ~U[2024-10-11 10:40:00Z]
- now = ~U[2020-04-06T10:00:00Z]
+ primary_departures = [
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s3"]}}}
+ ]
- fetch_schedules_fn = fn
- _, nil ->
- {:ok,
- [
- %Schedule{
- departure_time: ~U[2020-04-06T09:00:00Z],
- route: %Route{id: "Bus B"},
- stop: struct(Stop, id: "bus-B")
- }
- ]}
+ expected_first_schedule =
+ %Schedule{
+ arrival_time: ~U[2024-10-11 10:27:00Z],
+ departure_time: ~U[2024-10-11 10:30:00Z],
+ route: %Route{id: "r1", line: %Line{id: "l1"}, type: :subway},
+ stop: %Stop{id: "s1"},
+ trip: %Trip{headsign: "other1", pattern_headsign: "h1"}
+ }
- _, _ ->
- {:ok,
- [
- %Schedule{
- departure_time: ~U[2020-03-07T09:00:00Z],
- route: %Route{id: "Bus B"},
- stop: struct(Stop, id: "bus-B")
- }
- ]}
- end
+ expected_last_schedule =
+ %Schedule{
+ departure_time: ~U[2024-10-11 10:50:00Z],
+ route: %Route{id: "r3", line: %Line{id: "l3"}, type: :subway},
+ stop: %Stop{id: "s3"},
+ trip: %Trip{headsign: "other3", pattern_headsign: "h3"}
+ }
- expected_instances = [
- %DeparturesNoData{screen: config, slot_name: :main_content_zero},
- %DeparturesNoData{screen: config, slot_name: :main_content_one},
- %DeparturesNoData{screen: config, slot_name: :main_content_two}
+ expected_departures = [
+ {expected_first_schedule, :first_trip},
+ {expected_last_schedule, :service_ended}
]
- actual_instances =
- Dup.Departures.departures_instances(
- config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
-
- assert Enum.all?(expected_instances, &Enum.member?(actual_instances, &1))
- end
+ expected_primary_sections = [
+ %Screens.V2.WidgetInstance.Departures.NormalSection{
+ header: %ScreensConfig.Departures.Header{
+ arrow: nil,
+ read_as: nil,
+ subtitle: nil,
+ title: nil
+ },
+ layout: %ScreensConfig.Departures.Layout{
+ base: nil,
+ include_later: false,
+ max: nil,
+ min: 1
+ },
+ grouping_type: :time,
+ rows: expected_departures
+ }
+ ]
- test "returns no-data if now is before today's last schedule and there are no schedules tomorrow",
- %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_alerts_fn: fetch_alerts_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- } do
config =
- config
- |> put_primary_departures([
- %Section{
- query: %Query{params: %Query.Params{stop_ids: ["bus-B"]}}
- }
- ])
+ @config
+ |> put_primary_departures(primary_departures)
- now = ~U[2020-04-06T08:00:00Z]
-
- fetch_schedules_fn = fn
- _, nil ->
+ expect(@rds, :get, fn _primary_departures, _now ->
+ [
{:ok,
[
- %Schedule{
- departure_time: ~U[2020-04-06T09:00:00Z],
- route: %Route{id: "Bus B"},
- stop: struct(Stop, id: "bus-B")
- }
+ service_ended("s1", "l1", "other1", expected_last_schedule),
+ first_trip("s3", "l3", "other3", expected_first_schedule)
]}
+ ]
+ end)
- _, _ ->
- {:ok, []}
- end
-
- expected_instances = [
- %DeparturesNoData{screen: config, slot_name: :main_content_zero},
- %DeparturesNoData{screen: config, slot_name: :main_content_one},
- %DeparturesNoData{screen: config, slot_name: :main_content_two}
- ]
+ expect(@rds, :get, fn _primary_departures, _now -> [{:ok, []}] end)
- actual_instances =
- Dup.Departures.departures_instances(
+ expected_instances =
+ expected_departures_widget(
config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
+ expected_primary_sections,
+ expected_primary_sections,
+ now
)
- assert Enum.all?(expected_instances, &Enum.member?(actual_instances, &1))
+ actual_instances = Dup.Departures.instances(config, now)
+
+ assert actual_instances == expected_instances
end
- test "returns OvernightDepartures if all routes in section are overnight",
- %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_alerts_fn: fetch_alerts_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- } do
- config =
- config
- |> put_primary_departures([
- %Section{
- query: %Query{params: %Query.Params{stop_ids: ["place-A"]}}
- }
- ])
- |> put_secondary_departures_sections([
- %Section{
- query: %Query{params: %Query.Params{stop_ids: ["bus-B"]}}
- }
- ])
+ test "creates OvernightSection for service ended in a section" do
+ now = ~U[2024-10-11 10:40:00Z]
- now = ~U[2020-04-06T10:00:00Z]
+ primary_departures = [
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s3"]}}}
+ ]
- fetch_schedules_fn = fn
- _, nil ->
- {:ok,
- [
- %Schedule{
- departure_time: ~U[2020-04-06T09:00:00Z],
- route: %Route{id: "Bus B"},
- stop: struct(Stop, id: "bus-B")
- }
- ]}
+ expected_first_schedule =
+ %Schedule{
+ arrival_time: ~U[2024-10-11 10:50:00Z],
+ departure_time: ~U[2024-10-11 10:52:00Z],
+ route: %Route{id: "r1", line: %Line{id: "l1"}, type: :subway},
+ stop: %Stop{id: "s1"},
+ trip: %Trip{headsign: "other1", pattern_headsign: "h1"}
+ }
- _, _ ->
- {:ok,
- [
- %Schedule{
- departure_time: ~U[2020-04-07T09:00:00Z],
- route: %Route{id: "Bus B"},
- stop: struct(Stop, id: "bus-B")
- }
- ]}
- end
+ expected_last_schedule =
+ %Schedule{
+ departure_time: ~U[2024-10-11 10:38:00Z],
+ route: %Route{id: "r3", line: %Line{id: "l3"}, type: :subway},
+ stop: %Stop{id: "s3"},
+ trip: %Trip{headsign: "other3", pattern_headsign: "h3"}
+ }
- expected_departures = [
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_zero],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_one],
- now: now
+ expected_departures = [{expected_first_schedule, :first_trip}]
+
+ expected_sections = [
+ %Screens.V2.WidgetInstance.Departures.NormalSection{
+ header: %ScreensConfig.Departures.Header{
+ arrow: nil,
+ read_as: nil,
+ subtitle: nil,
+ title: nil
+ },
+ layout: %ScreensConfig.Departures.Layout{
+ base: nil,
+ include_later: false,
+ max: nil,
+ min: 1
+ },
+ grouping_type: :time,
+ rows: expected_departures
},
- %OvernightDepartures{screen: config, routes: [:bus], slot_names: [:main_content_two]}
+ %Screens.V2.WidgetInstance.Departures.OvernightSection{
+ routes: [
+ %Screens.Routes.Route{id: "r3", type: :subway, line: %Screens.Lines.Line{id: "l3"}}
+ ]
+ }
]
- actual_instances =
- Dup.Departures.departures_instances(
- config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
-
- assert Enum.all?(expected_departures, &Enum.member?(actual_instances, &1))
- end
-
- test "returns OvernightDepartures with no routes if all rotations are overnight",
- %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_alerts_fn: fetch_alerts_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- } do
config =
- config
- |> put_primary_departures([
- %Section{
- query: %Query{params: %Query.Params{stop_ids: ["bus-B", "bus-C"]}}
- }
- ])
-
- now = ~U[2020-04-06T10:00:00Z]
+ @config
+ |> put_primary_departures(primary_departures)
- fetch_schedules_fn = fn
- _, nil ->
+ expect(@rds, :get, fn _primary_departures, _now ->
+ [
{:ok,
[
- %Schedule{
- departure_time: ~U[2020-04-06T09:00:00Z],
- route: %Route{id: "Bus B"},
- stop: struct(Stop, id: "bus-B")
- },
- %Schedule{
- departure_time: ~U[2020-04-06T09:30:00Z],
- route: %Route{id: "Bus C"},
- stop: struct(Stop, id: "bus-C")
- }
- ]}
-
- _, _ ->
+ first_trip("s1", "l3", "other3", expected_first_schedule)
+ ]},
{:ok,
[
- %Schedule{
- departure_time: ~U[2020-04-07T09:00:00Z],
- route: %Route{id: "Bus B"},
- stop: struct(Stop, id: "bus-B")
- },
- %Schedule{
- departure_time: ~U[2020-04-07T09:00:00Z],
- route: %Route{id: "Bus C"},
- stop: struct(Stop, id: "bus-C")
- }
+ service_ended("s3", "l1", "other1", expected_last_schedule)
]}
- end
+ ]
+ end)
- expected_departures = [
- %OvernightDepartures{screen: config, routes: [], slot_names: [:main_content_zero]},
- %OvernightDepartures{screen: config, routes: [], slot_names: [:main_content_one]},
- %OvernightDepartures{screen: config, routes: [], slot_names: [:main_content_two]}
- ]
+ expect(@rds, :get, fn _secondary_departures, _now -> [{:ok, []}] end)
- actual_instances =
- Dup.Departures.departures_instances(
+ expected_instances =
+ expected_departures_widget(
config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
+ expected_sections,
+ expected_sections,
+ now
)
- assert Enum.all?(expected_departures, &Enum.member?(actual_instances, &1))
+ actual_instances = Dup.Departures.instances(config, now)
+
+ assert actual_instances == expected_instances
end
- test "doesn't return OvernightDepartures for rail sections without active vehicles but with an active alert",
- %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_routes_fn: fetch_routes_fn
- } do
- config =
- config
- |> put_primary_departures([
- %Section{
- query: %Query{params: %Query.Params{stop_ids: ["place-overnight"]}}
- }
- ])
+ test "creates OvernightDepartures when all modes have service ended" do
+ now = ~U[2024-10-11 10:40:00Z]
+
+ primary_departures = [
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s3"]}}}
+ ]
+
+ expected_last_schedule_one =
+ %Schedule{
+ arrival_time: ~U[2024-10-11 10:38:00Z],
+ departure_time: ~U[2024-10-11 10:39:00Z],
+ route: %Route{id: "r1", line: %Line{id: "l1"}, type: :subway},
+ stop: %Stop{id: "s1"},
+ trip: %Trip{headsign: "other1", pattern_headsign: "h1"}
+ }
- now = ~U[2020-04-06T10:00:00Z]
- stub(@headways, :get_with_route, fn "place-overnight", "Red", ^now -> {5, 8} end)
+ expected_last_schedule_two =
+ %Schedule{
+ departure_time: ~U[2024-10-11 10:38:00Z],
+ route: %Route{id: "r3", line: %Line{id: "l3"}, type: :subway},
+ stop: %Stop{id: "s3"},
+ trip: %Trip{headsign: "other3", pattern_headsign: "h3"}
+ }
+
+ config =
+ @config
+ |> put_primary_departures(primary_departures)
- fetch_schedules_fn = fn
- _, nil ->
+ expect(@rds, :get, fn _primary_departures, _now ->
+ [
{:ok,
[
- %Schedule{
- departure_time: ~U[2020-04-06T09:00:00Z],
- route: %Route{id: "Red"},
- stop: struct(Stop, id: "place-overnight")
- }
- ]}
-
- _, _ ->
+ service_ended("s1", "l1", "other1", expected_last_schedule_one)
+ ]},
{:ok,
[
- %Schedule{
- departure_time: ~U[2020-04-07T09:00:00Z],
- route: %Route{id: "Red"},
- stop: struct(Stop, id: "place-overnight")
- }
+ service_ended("s3", "l3", "other3", expected_last_schedule_two)
]}
- end
+ ]
+ end)
- fetch_alerts_fn = fn
- [
- direction_id: :both,
- route_ids: [],
- stop_ids: ["place-overnight"],
- route_types: [:light_rail, :subway]
- ] ->
- [
- struct(Alert,
- effect: :suspension,
- informed_entities: [ie(stop_id: "place-overnight", route: "Red", route_type: 0)],
- active_period: [{~U[2020-04-06T09:00:00Z], nil}]
- )
- ]
- end
+ expect(@rds, :get, fn _secondary_departures, _now -> [{:ok, []}] end)
- fetch_vehicles_fn = fn _, _ -> [] end
+ expected_instances = expected_overnight_departures_widget(config)
- expected_departures = [
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: []
- }
- ],
- slot_names: [:main_content_zero],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: []
- }
- ],
- slot_names: [:main_content_one],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: []
- }
- ],
- slot_names: [:main_content_two],
- now: now
- }
+ actual_instances = Dup.Departures.instances(config, now)
+
+ assert actual_instances == expected_instances
+ end
+
+ test "creates HeadwaySections for destinations with headways" do
+ primary_departures = [
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s2"]}}}
]
- actual_instances =
- Dup.Departures.departures_instances(
- config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
+ expected_route_id = "r1"
+ expected_time_range = {6, 10}
+ expected_headsign = "headsign"
- assert Enum.all?(expected_departures, &Enum.member?(actual_instances, &1))
- end
+ expected_primary_sections = [
+ %Screens.V2.WidgetInstance.Departures.HeadwaySection{
+ headsign: expected_headsign,
+ route: expected_route_id,
+ time_range: expected_time_range
+ }
+ ]
- test "returns primary section Departures if not all routes in secondary section are overnight, but none have upcoming predictions",
- %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_alerts_fn: fetch_alerts_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- } do
config =
- config
- |> put_primary_departures([
- %Section{
- query: %Query{params: %Query.Params{stop_ids: ["place-A"]}}
- }
- ])
- |> put_secondary_departures_sections([
- %Section{query: %Query{params: %Query.Params{stop_ids: ["bus-C+D"]}}}
- ])
+ @config
+ |> put_primary_departures(primary_departures)
- now = ~U[2020-04-06T10:00:00Z]
-
- fetch_schedules_fn = fn
- %{direction_id: :both, route_ids: [], route_type: nil, stop_ids: ["bus-C+D"]},
- ~D[2020-04-07] ->
+ expect(@rds, :get, fn _primary_departures, @now ->
+ [
{:ok,
[
- %Schedule{
- departure_time: ~U[2020-04-07T09:00:00Z],
- route: %Route{id: "Bus C"},
- stop: struct(Stop, id: "bus-C+D")
- },
- %Schedule{
- departure_time: ~U[2020-04-07T09:00:00Z],
- route: %Route{id: "Bus D"},
- stop: struct(Stop, id: "bus-C+D")
- }
+ headways(
+ "s1",
+ "l1",
+ "headsign",
+ expected_route_id,
+ "Northbound",
+ 0,
+ expected_time_range
+ ),
+ headways(
+ "s1",
+ "l1",
+ "headsign",
+ expected_route_id,
+ "Northbound",
+ 0,
+ expected_time_range
+ )
]}
+ ]
+ end)
- _, _ ->
- {:ok,
- [
- %Schedule{
- departure_time: ~U[2020-04-06T09:00:00Z],
- route: %Route{id: "Bus C"},
- stop: struct(Stop, id: "bus-C+D")
- },
- %Schedule{
- departure_time: ~U[2020-04-06T09:00:00Z],
- route: %Route{id: "Bus D"},
- stop: struct(Stop, id: "bus-C+D")
- },
- %Schedule{
- departure_time: ~U[2020-04-06T11:01:00Z],
- route: %Route{id: "Bus D"},
- stop: struct(Stop, id: "bus-C+D")
- }
- ]}
- end
+ expect(@rds, :get, fn _primary_departures, @now -> [{:ok, []}] end)
- expected_departures = [
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_zero],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_one],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_two],
- now: now
- }
- ]
+ expected_instances =
+ expected_departures_widget(config, expected_primary_sections, expected_primary_sections)
- actual_instances =
- Dup.Departures.departures_instances(
- config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
+ actual_instances = Dup.Departures.instances(config, @now)
- assert Enum.all?(expected_departures, &Enum.member?(actual_instances, &1))
+ assert actual_instances == expected_instances
end
- test "returns primary section Departures if routes in secondary section have no predictions for today or schedules for tomorrow",
- %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_alerts_fn: fetch_alerts_fn,
- fetch_routes_fn: fetch_routes_fn,
- fetch_vehicles_fn: fetch_vehicles_fn
- } do
- config =
- config
- |> put_primary_departures([
- %Section{
- query: %Query{params: %Query.Params{stop_ids: ["place-A"]}}
- }
- ])
- |> put_secondary_departures_sections([
- %Section{query: %Query{params: %Query.Params{stop_ids: ["bus-C"]}}}
- ])
+ test "creates HeadwaySections for destinations with headways with different headsigns" do
+ primary_departures = [
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s2"]}}}
+ ]
+
+ expected_route_id = "r1"
+ expected_time_range = {6, 10}
+ expected_direction_name = "Northbound"
- now = ~U[2020-04-06T10:00:00Z]
+ expected_primary_sections = [
+ %Screens.V2.WidgetInstance.Departures.HeadwaySection{
+ headsign: expected_direction_name,
+ route: expected_route_id,
+ time_range: expected_time_range
+ }
+ ]
- fetch_schedules_fn = fn
- %{direction_id: :both, route_ids: [], route_type: nil, stop_ids: ["bus-C"]},
- ~D[2020-04-07] ->
- {:ok, []}
+ config =
+ @config
+ |> put_primary_departures(primary_departures)
- _, _ ->
+ expect(@rds, :get, fn _primary_departures, @now ->
+ [
{:ok,
[
- %Schedule{
- departure_time: ~U[2020-04-06T09:00:00Z],
- route: %Route{id: "Bus C"},
- stop: struct(Stop, id: "bus-C")
- },
- %Schedule{
- departure_time: ~U[2020-04-06T11:00:00Z],
- route: %Route{id: "Bus C"},
- stop: struct(Stop, id: "bus-C")
- }
+ headways(
+ "s1",
+ "l1",
+ "headsign",
+ expected_route_id,
+ expected_direction_name,
+ 0,
+ expected_time_range
+ ),
+ headways(
+ "s1",
+ "l1",
+ "other_headsign",
+ expected_route_id,
+ expected_direction_name,
+ 0,
+ expected_time_range
+ )
]}
- end
+ ]
+ end)
- expected_departures = [
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_zero],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_one],
- now: now
- },
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "A",
- route: %Route{id: "Test"},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- slot_names: [:main_content_two],
- now: now
- }
- ]
+ expect(@rds, :get, fn _primary_departures, @now -> [{:ok, []}] end)
- actual_instances =
- Dup.Departures.departures_instances(
- config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
+ expected_instances =
+ expected_departures_widget(config, expected_primary_sections, expected_primary_sections)
- assert Enum.all?(expected_departures, &Enum.member?(actual_instances, &1))
+ actual_instances = Dup.Departures.instances(config, @now)
+
+ assert actual_instances == expected_instances
end
- end
- describe "departures_instances/4 alert handling" do
- test "returns no departures for a section when alerts indicate that all routes are expected to be closed",
- %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_routes_fn: fetch_routes_fn
- } do
- config =
- config
- |> put_primary_departures([
- %Section{
- query: %Query{
- params: %Query.Params{stop_ids: ["place-closed"], route_ids: ["Red"]}
- }
+ test "creates NormalSections for departures and headways" do
+ expected_route_id = "r1"
+ expected_time_range = {6, 10}
+ expected_direction_name = "Northbound"
+
+ primary_departures = [
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s2"]}}}
+ ]
+
+ expected_primary_departure =
+ %Departure{
+ prediction: %Prediction{
+ arrival_time: ~U[2024-10-11 12:27:00Z],
+ departure_time: ~U[2024-10-11 12:30:00Z],
+ route: %Route{id: "r1", line: %Line{id: "l1"}, type: :bus},
+ stop: %Stop{id: "s1"},
+ trip: %Trip{headsign: "other1", pattern_headsign: "h1", direction_id: 1}
},
- %Section{
- query: %Query{
- params: %Query.Params{stop_ids: ["bus-A", "bus-B"], route_ids: ["Bus A"]}
+ schedule: nil
+ }
+
+ expected_primary_sections = [
+ %Screens.V2.WidgetInstance.Departures.NormalSection{
+ header: %ScreensConfig.Departures.Header{
+ arrow: nil,
+ read_as: nil,
+ subtitle: nil,
+ title: nil
+ },
+ layout: %ScreensConfig.Departures.Layout{
+ base: nil,
+ include_later: false,
+ max: nil,
+ min: 1
+ },
+ grouping_type: :time,
+ rows: [
+ expected_primary_departure,
+ %HeadwayRow{
+ line: %Line{id: "l1"},
+ direction_id: 0,
+ range: {6, 10},
+ headsign: "other_headsign"
}
- }
- ])
+ ]
+ }
+ ]
- now = ~U[2020-04-06T18:00:00Z]
+ config =
+ @config
+ |> put_primary_departures(primary_departures)
- fetch_schedules_fn = fn
- _, ~D[2020-04-07] ->
+ expect(@rds, :get, fn _primary_departures, @now ->
+ [
{:ok,
[
- %Schedule{
- departure_time: ~U[2020-04-07T09:00:00Z],
- route: %Route{id: "Red"},
- stop: struct(Stop, id: "place-closed")
- }
+ rds_countdown("s1", "l1", "other1", [expected_primary_departure]),
+ headways(
+ "s1",
+ "l1",
+ "other_headsign",
+ expected_route_id,
+ expected_direction_name,
+ 0,
+ expected_time_range
+ )
]}
+ ]
+ end)
- _, ^now ->
- {:ok,
- [
- %Schedule{
- departure_time: ~U[2020-04-06T09:00:00Z],
- route: %Route{id: "Red"},
- stop: struct(Stop, id: "place-closed")
- },
- %Schedule{
- departure_time: ~U[2020-04-07T03:00:00Z],
- route: %Route{id: "Red"},
- stop: struct(Stop, id: "place-closed")
- }
- ]}
- end
+ expect(@rds, :get, fn _secondary_departures, @now -> [{:ok, []}] end)
- fetch_alerts_fn = fn
- [
- direction_id: :both,
- route_ids: ["Red"],
- stop_ids: ["place-closed"],
- route_types: [:light_rail, :subway]
- ] ->
- [
- struct(Alert,
- effect: :suspension,
- informed_entities: [ie(stop_id: "place-closed", route: "Red", route_type: 0)],
- active_period: [{~U[2020-04-06T09:00:00Z], nil}]
- )
- ]
+ expected_instances =
+ expected_departures_widget(config, expected_primary_sections, expected_primary_sections)
- _ ->
- []
- end
+ actual_instances = Dup.Departures.instances(config, @now)
- fetch_vehicles_fn = fn _, _ -> [] end
+ assert actual_instances == expected_instances
+ end
- expected_departures = [
- %DeparturesWidget{
- screen: config,
- sections: [
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: []
- },
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "Bus A",
- route: %Route{id: "Bus A", type: :bus},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- now: now,
- slot_names: [:main_content_reduced_two]
- }
- ]
+ test "creates NormalSections but removes headways when similar line/direction_id destinations in Countdown state" do
+ expected_route_id = "r1"
+ expected_time_range = {6, 10}
+ expected_direction_name = "Northbound"
- actual_instances =
- Dup.Departures.departures_instances(
- config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
+ primary_departures = [
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s2"]}}}
+ ]
- assert(Enum.all?(expected_departures, &Enum.member?(actual_instances, &1)))
- end
+ expected_primary_departure =
+ %Departure{
+ prediction: %Prediction{
+ arrival_time: ~U[2024-10-11 12:27:00Z],
+ departure_time: ~U[2024-10-11 12:30:00Z],
+ route: %Route{id: "r1", line: %Line{id: "l1"}, type: :subway},
+ stop: %Stop{id: "s1"},
+ trip: %Trip{headsign: "other1", pattern_headsign: "h1", direction_id: 0}
+ },
+ schedule: nil
+ }
- test "returns NoDataSection when alerts do not cover all directions for which we are missing departures",
- %{
- config: config,
- fetch_departures_fn: fetch_departures_fn,
- fetch_routes_fn: fetch_routes_fn
- } do
- config =
- config
- |> put_primary_departures([
- %Section{
- query: %Query{
- params: %Query.Params{stop_ids: ["place-closed"], route_ids: ["Red"]}
- }
+ expected_primary_sections = [
+ %Screens.V2.WidgetInstance.Departures.NormalSection{
+ header: %ScreensConfig.Departures.Header{
+ arrow: nil,
+ read_as: nil,
+ subtitle: nil,
+ title: nil
},
- %Section{
- query: %Query{
- params: %Query.Params{stop_ids: ["bus-A", "bus-B"], route_ids: ["Bus A"]}
- }
- }
- ])
+ layout: %ScreensConfig.Departures.Layout{
+ base: nil,
+ include_later: false,
+ max: nil,
+ min: 1
+ },
+ grouping_type: :time,
+ rows: [
+ expected_primary_departure
+ ]
+ }
+ ]
- now = ~U[2020-04-06T18:00:00Z]
+ config =
+ @config
+ |> put_primary_departures(primary_departures)
- fetch_schedules_fn = fn
- _, ~D[2020-04-07] ->
+ expect(@rds, :get, fn _primary_departures, @now ->
+ [
{:ok,
[
- %Schedule{
- departure_time: ~U[2020-04-07T09:00:00Z],
- route: %Route{id: "Red"},
- stop: struct(Stop, id: "place-closed")
- }
+ rds_countdown("s1", "l1", "other1", [expected_primary_departure]),
+ headways(
+ "s1",
+ "l1",
+ "other_headsign",
+ expected_route_id,
+ expected_direction_name,
+ 0,
+ expected_time_range
+ )
]}
+ ]
+ end)
- _, ^now ->
- {:ok,
- [
- %Schedule{
- departure_time: ~U[2020-04-06T09:00:00Z],
- route: %Route{id: "Red"},
- stop: struct(Stop, id: "place-closed")
- },
- %Schedule{
- departure_time: ~U[2020-04-07T03:00:00Z],
- route: %Route{id: "Red"},
- stop: struct(Stop, id: "place-closed")
- }
- ]}
- end
+ expect(@rds, :get, fn _secondary_departures, @now -> [{:ok, []}] end)
- fetch_alerts_fn = fn
- [
- direction_id: :both,
- route_ids: ["Red"],
- stop_ids: ["place-closed"],
- route_types: [:light_rail, :subway]
- ] ->
- [
- struct(Alert,
- effect: :suspension,
- informed_entities: [
- ie(stop_id: "place-closed", route: "Red", route_type: 0, direction_id: 0)
- ],
- active_period: [{~U[2020-04-06T09:00:00Z], nil}]
- )
- ]
+ expected_instances =
+ expected_departures_widget(config, expected_primary_sections, expected_primary_sections)
- _ ->
- []
- end
+ actual_instances = Dup.Departures.instances(config, @now)
- fetch_vehicles_fn = fn _, _ -> [] end
+ assert actual_instances == expected_instances
+ end
- expected_departures = [
- %DeparturesWidget{
- screen: config,
- sections: [
- %NoDataSection{route: %{id: "Red", type: :subway}},
- %NormalSection{
- layout: %Layout{},
- header: %SectionHeader{},
- rows: [
- %Screens.V2.Departure{
- prediction:
- struct(Prediction,
- id: "Bus A",
- route: %Route{id: "Bus A", type: :bus},
- stop: struct(Stop),
- trip: struct(Trip)
- ),
- schedule: nil
- }
- ]
- }
- ],
- now: now,
- slot_names: [:main_content_zero]
+ test "creates NormalSections but removes headways when similar line/direction_id destinations are in No Service state" do
+ expected_route_id = "r1"
+ expected_time_range = {6, 10}
+ expected_direction_name = "Northbound"
+
+ primary_departures = [
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
+ %Section{query: %Query{params: %Query.Params{stop_ids: ["s2"]}}}
+ ]
+
+ expected_primary_departure =
+ %Departure{
+ prediction: %Prediction{
+ arrival_time: ~U[2024-10-11 12:27:00Z],
+ departure_time: ~U[2024-10-11 12:30:00Z],
+ route: %Route{id: "r1", line: %Line{id: "l1"}, type: :subway},
+ stop: %Stop{id: "s1"},
+ trip: %Trip{headsign: "other1", pattern_headsign: "h1", direction_id: 0}
+ },
+ schedule: nil
+ }
+
+ expected_primary_sections = [
+ %Screens.V2.WidgetInstance.Departures.NormalSection{
+ header: %ScreensConfig.Departures.Header{
+ arrow: nil,
+ read_as: nil,
+ subtitle: nil,
+ title: nil
+ },
+ layout: %ScreensConfig.Departures.Layout{
+ base: nil,
+ include_later: false,
+ max: nil,
+ min: 1
+ },
+ grouping_type: :time,
+ rows: [
+ expected_primary_departure
+ ]
}
]
- actual_instances =
- Dup.Departures.departures_instances(
- config,
- now,
- fetch_departures_fn,
- fetch_alerts_fn,
- fetch_schedules_fn,
- fetch_routes_fn,
- fetch_vehicles_fn
- )
+ config =
+ @config
+ |> put_primary_departures(primary_departures)
+
+ expect(@rds, :get, fn _primary_departures, @now ->
+ [
+ {:ok,
+ [
+ rds_countdown("s1", "l1", "other1", [expected_primary_departure]),
+ no_service("s1", "l2", "some_headsign", [
+ %Screens.Routes.Route{
+ id: "r1",
+ short_name: nil,
+ long_name: nil,
+ direction_names: nil,
+ direction_destinations: nil,
+ type: :bus,
+ line: %Screens.Lines.Line{
+ id: "l2",
+ long_name: nil,
+ short_name: nil,
+ sort_order: nil
+ }
+ }
+ ]),
+ headways(
+ "s1",
+ "l2",
+ "other_headsign",
+ expected_route_id,
+ expected_direction_name,
+ 0,
+ expected_time_range
+ )
+ ]}
+ ]
+ end)
+
+ expect(@rds, :get, fn _secondary_departures, @now -> [{:ok, []}] end)
+
+ expected_instances =
+ expected_departures_widget(config, expected_primary_sections, expected_primary_sections)
+
+ actual_instances = Dup.Departures.instances(config, @now)
- assert(Enum.all?(expected_departures, &Enum.member?(actual_instances, &1)))
+ assert actual_instances == expected_instances
end
end
end
diff --git a/test/screens/v2/candidate_generator/dup_new/departures_test.exs b/test/screens/v2/candidate_generator/dup_new/departures_test.exs
deleted file mode 100644
index 902c7b00e..000000000
--- a/test/screens/v2/candidate_generator/dup_new/departures_test.exs
+++ /dev/null
@@ -1,1338 +0,0 @@
-defmodule Screens.V2.CandidateGenerator.DupNew.DeparturesTest do
- use ExUnit.Case, async: true
-
- alias Screens.Lines.Line
- alias Screens.Predictions.Prediction
- alias Screens.Routes.Route
- alias Screens.Schedules.Schedule
- alias Screens.Stops.Stop
- alias Screens.Trips.Trip
- alias Screens.V2.CandidateGenerator.DupNew
- alias Screens.V2.Departure
- alias Screens.V2.RDS
- alias Screens.V2.WidgetInstance.Departures, as: DeparturesWidget
- alias Screens.V2.WidgetInstance.Departures.HeadwayRow
- alias Screens.V2.WidgetInstance.{DeparturesNoData, DeparturesNoService}
- alias Screens.V2.WidgetInstance.OvernightDepartures
- alias ScreensConfig.{Alerts, Departures, Header}
- alias ScreensConfig.Departures.{Query, Section}
- alias ScreensConfig.Screen
- alias ScreensConfig.Screen.Dup, as: DupConfig
-
- import Mox
- import Screens.Inject
-
- setup :verify_on_exit!
-
- @rds injected(RDS)
-
- @now ~U[2020-04-06T10:00:00Z]
- @config %Screen{
- app_params: %DupConfig{
- header: %Header.StopId{stop_id: "place-test"},
- primary_departures: %Departures{
- sections: []
- },
- secondary_departures: %Departures{
- sections: []
- },
- alerts: struct(Alerts)
- },
- vendor: :outfront,
- device_id: "TEST",
- name: "TEST",
- app_id: :dup_v2
- }
-
- defp rds_countdown(stop_id, line_id, headsign, expected_departures) do
- %RDS{
- stop: %Stop{id: stop_id},
- line: %Line{id: line_id},
- headsign: headsign,
- state: %RDS.Countdowns{
- departures: expected_departures
- }
- }
- end
-
- defp no_service(stop_id, line_id, headsign, routes \\ []) do
- %RDS{
- stop: %Stop{id: stop_id},
- line: %Line{id: line_id},
- headsign: headsign,
- state: %RDS.NoService{routes: routes, direction_id: 0}
- }
- end
-
- defp first_trip(stop_id, line_id, headsign, first_scheduled) do
- %RDS{
- stop: %Stop{id: stop_id},
- line: %Line{id: line_id},
- headsign: headsign,
- state: %RDS.FirstTrip{
- first_scheduled_departure: %Departure{
- prediction: nil,
- schedule: first_scheduled
- }
- }
- }
- end
-
- defp service_ended(stop_id, line_id, headsign, last_scheduled) do
- %RDS{
- stop: %Stop{id: stop_id},
- line: %Line{id: line_id},
- headsign: headsign,
- state: %RDS.ServiceEnded{
- last_scheduled_departure: %Departure{
- prediction: nil,
- schedule: last_scheduled
- }
- }
- }
- end
-
- defp headways(stop_id, line_id, headsign, route_id, direction_name, direction_id, range) do
- %RDS{
- stop: %Stop{id: stop_id},
- line: %Line{id: line_id},
- headsign: headsign,
- state: %RDS.Headways{
- departure_id: "Test ID",
- route_id: route_id,
- direction_name: direction_name,
- direction_id: direction_id,
- range: range
- }
- }
- end
-
- defp expected_departures_widget(
- config,
- expected_primary_sections,
- expected_secondary_sections,
- now \\ @now
- ) do
- [
- %DeparturesWidget{
- screen: config,
- slot_names: [:main_content_zero],
- now: now,
- sections: expected_primary_sections
- },
- %DeparturesWidget{
- screen: config,
- slot_names: [:main_content_one],
- now: now,
- sections: expected_primary_sections
- },
- %DeparturesWidget{
- screen: config,
- slot_names: [:main_content_reduced_zero],
- now: now,
- sections: expected_primary_sections
- },
- %DeparturesWidget{
- screen: config,
- slot_names: [:main_content_reduced_one],
- now: now,
- sections: expected_primary_sections
- },
- %DeparturesWidget{
- screen: config,
- slot_names: [:main_content_two],
- now: now,
- sections: expected_secondary_sections
- },
- %DeparturesWidget{
- screen: config,
- slot_names: [:main_content_reduced_two],
- now: now,
- sections: expected_secondary_sections
- }
- ]
- end
-
- defp expected_overnight_departures_widget(config) do
- [
- %OvernightDepartures{screen: config, slot_names: [:main_content_zero]},
- %OvernightDepartures{screen: config, slot_names: [:main_content_one]},
- %OvernightDepartures{screen: config, slot_names: [:main_content_reduced_zero]},
- %OvernightDepartures{screen: config, slot_names: [:main_content_reduced_one]},
- %OvernightDepartures{screen: config, slot_names: [:main_content_two]},
- %OvernightDepartures{screen: config, slot_names: [:main_content_reduced_two]}
- ]
- end
-
- defp put_primary_departures(config, primary_departures_sections) do
- %{
- config
- | app_params: %{
- config.app_params
- | primary_departures: %Departures{sections: primary_departures_sections}
- }
- }
- end
-
- defp put_secondary_departures_sections(config, secondary_departures_sections) do
- %{
- config
- | app_params: %{
- config.app_params
- | secondary_departures: %Departures{sections: secondary_departures_sections}
- }
- }
- end
-
- setup do
- stub(@rds, :get, fn _, _ -> [{:ok, []}] end)
- :ok
- end
-
- describe "instances/3" do
- test "returns DeparturesNoData on RDS returning errors" do
- primary_departures = [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-A"]}}},
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-B"]}}}
- ]
-
- secondary_departures = [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-C"]}}},
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-D"]}}}
- ]
-
- config =
- @config
- |> put_primary_departures(primary_departures)
- |> put_secondary_departures_sections(secondary_departures)
-
- expect(@rds, :get, fn _primary_departures, @now -> [:error, :error] end)
- expect(@rds, :get, fn _secondary_departures, @now -> [:error, :error] end)
-
- expected_instances = [
- %DeparturesNoData{screen: config, slot_name: :main_content_zero},
- %DeparturesNoData{screen: config, slot_name: :main_content_one},
- %DeparturesNoData{screen: config, slot_name: :main_content_reduced_zero},
- %DeparturesNoData{screen: config, slot_name: :main_content_reduced_one},
- %DeparturesNoData{screen: config, slot_name: :main_content_two},
- %DeparturesNoData{screen: config, slot_name: :main_content_reduced_two}
- ]
-
- actual_instances = DupNew.Departures.instances(config, @now)
-
- assert actual_instances == expected_instances
- end
-
- test "returns DeparturesNoService on RDS returning NoService states" do
- primary_departures = [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-A"]}}},
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-B"]}}}
- ]
-
- secondary_departures = [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-C"]}}},
- %Section{query: %Query{params: %Query.Params{stop_ids: ["place-D"]}}}
- ]
-
- config =
- @config
- |> put_primary_departures(primary_departures)
- |> put_secondary_departures_sections(secondary_departures)
-
- expect(@rds, :get, fn _primary_departures, @now ->
- [
- {:ok, [no_service("place-A", "line-A", "headsign")]},
- {:ok, [no_service("place-B", "line-B", "headsign2")]}
- ]
- end)
-
- expect(@rds, :get, fn _secondary_departures, @now -> [] end)
-
- expected_instances = [
- %DeparturesNoService{screen: config, slot_name: :main_content_zero},
- %DeparturesNoService{screen: config, slot_name: :main_content_one},
- %DeparturesNoService{screen: config, slot_name: :main_content_reduced_zero},
- %DeparturesNoService{screen: config, slot_name: :main_content_reduced_one},
- %DeparturesNoService{screen: config, slot_name: :main_content_two},
- %DeparturesNoService{screen: config, slot_name: :main_content_reduced_two}
- ]
-
- actual_instances = DupNew.Departures.instances(config, @now)
-
- assert actual_instances == expected_instances
- end
-
- test "secondary departures fallback to primary departures when empty" do
- primary_departures = [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s2"]}}}
- ]
-
- secondary_departures = []
-
- expected_departures = [
- %Departure{
- prediction: %Prediction{
- arrival_time: ~U[2024-10-11 12:27:00Z],
- departure_time: ~U[2024-10-11 12:30:00Z],
- route: %Route{id: "r1", line: %Line{id: "l1"}, type: :bus},
- stop: %Stop{id: "s1"},
- trip: %Trip{headsign: "other1", pattern_headsign: "h1"}
- },
- schedule: nil
- }
- ]
-
- expected_primary_sections = [
- %Screens.V2.WidgetInstance.Departures.NormalSection{
- header: %ScreensConfig.Departures.Header{
- arrow: nil,
- read_as: nil,
- subtitle: nil,
- title: nil
- },
- layout: %ScreensConfig.Departures.Layout{
- base: nil,
- include_later: false,
- max: nil,
- min: 1
- },
- grouping_type: :time,
- rows: expected_departures
- }
- ]
-
- config =
- @config
- |> put_primary_departures(primary_departures)
- |> put_secondary_departures_sections(secondary_departures)
-
- expect(@rds, :get, fn _primary_departures, @now ->
- [
- {:ok, [rds_countdown("s1", "l1", "other1", expected_departures)]}
- ]
- end)
-
- expect(@rds, :get, fn _secondary_departures, @now -> [{:ok, []}, {:ok, []}] end)
-
- expected_instances =
- expected_departures_widget(config, expected_primary_sections, expected_primary_sections)
-
- actual_instances = DupNew.Departures.instances(config, @now)
-
- assert actual_instances == expected_instances
- end
-
- test "creates no service sections for no service states" do
- primary_departures = [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s2"]}}}
- ]
-
- secondary_departures = []
-
- expected_departures = [
- %Departure{
- prediction: %Prediction{
- arrival_time: ~U[2024-10-11 12:27:00Z],
- departure_time: ~U[2024-10-11 12:30:00Z],
- route: %Route{id: "r1", line: %Line{id: "l1"}, type: :bus},
- stop: %Stop{id: "s1"},
- trip: %Trip{headsign: "other1", pattern_headsign: "h1"}
- },
- schedule: nil
- }
- ]
-
- expected_primary_sections = [
- %Screens.V2.WidgetInstance.Departures.NormalSection{
- header: %ScreensConfig.Departures.Header{
- arrow: nil,
- read_as: nil,
- subtitle: nil,
- title: nil
- },
- layout: %ScreensConfig.Departures.Layout{
- base: nil,
- include_later: false,
- max: nil,
- min: 1
- },
- grouping_type: :time,
- rows: expected_departures
- },
- %Screens.V2.WidgetInstance.Departures.NoServiceSection{routes: []}
- ]
-
- config =
- @config
- |> put_primary_departures(primary_departures)
- |> put_secondary_departures_sections(secondary_departures)
-
- expect(@rds, :get, fn _primary_departures, @now ->
- [
- {:ok,
- [
- rds_countdown("s1", "l1", "other1", expected_departures)
- ]},
- {:ok, [no_service("s2", "l2", "other2")]}
- ]
- end)
-
- expect(@rds, :get, fn _secondary_departures, @now -> [{:ok, []}, {:ok, []}] end)
-
- expected_instances =
- expected_departures_widget(config, expected_primary_sections, expected_primary_sections)
-
- actual_instances = DupNew.Departures.instances(config, @now)
-
- assert actual_instances == expected_instances
- end
-
- test "creates no service section with routes for no service states that have routes" do
- primary_departures = [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s2"]}}}
- ]
-
- expected_route = %Route{id: "r1", line: %Line{id: "l1"}, type: :bus}
-
- secondary_departures = []
-
- expected_departures = [
- %Departure{
- prediction: %Prediction{
- arrival_time: ~U[2024-10-11 12:27:00Z],
- departure_time: ~U[2024-10-11 12:30:00Z],
- route: expected_route,
- stop: %Stop{id: "s1"},
- trip: %Trip{headsign: "other1", pattern_headsign: "h1"}
- },
- schedule: nil
- }
- ]
-
- expected_primary_sections = [
- %Screens.V2.WidgetInstance.Departures.NormalSection{
- header: %ScreensConfig.Departures.Header{
- arrow: nil,
- read_as: nil,
- subtitle: nil,
- title: nil
- },
- layout: %ScreensConfig.Departures.Layout{
- base: nil,
- include_later: false,
- max: nil,
- min: 1
- },
- grouping_type: :time,
- rows: expected_departures
- },
- %Screens.V2.WidgetInstance.Departures.NoServiceSection{routes: [expected_route]}
- ]
-
- config =
- @config
- |> put_primary_departures(primary_departures)
- |> put_secondary_departures_sections(secondary_departures)
-
- expect(@rds, :get, fn _primary_departures, @now ->
- [
- {:ok, [rds_countdown("s1", "l1", "other1", expected_departures)]},
- {:ok, [no_service("s2", "l2", "other2", [expected_route])]}
- ]
- end)
-
- expect(@rds, :get, fn _secondary_departures, @now -> [{:ok, []}, {:ok, []}] end)
-
- expected_instances =
- expected_departures_widget(config, expected_primary_sections, expected_primary_sections)
-
- actual_instances = DupNew.Departures.instances(config, @now)
-
- assert actual_instances == expected_instances
- end
-
- test "creates NormalSections for upcoming predictions and schedules" do
- primary_departures = [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s2"]}}}
- ]
-
- secondary_departures = [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s3"]}}},
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s4"]}}}
- ]
-
- expected_primary_departures = [
- %Departure{
- prediction: %Prediction{
- arrival_time: ~U[2024-10-11 12:27:00Z],
- departure_time: ~U[2024-10-11 12:30:00Z],
- route: %Route{id: "r1", line: %Line{id: "l1"}, type: :bus},
- stop: %Stop{id: "s1"},
- trip: %Trip{headsign: "other1", pattern_headsign: "h1"}
- },
- schedule: nil
- }
- ]
-
- expected_secondary_departures = [
- %Departure{
- prediction: nil,
- schedule: %Schedule{
- departure_time: ~U[2024-10-11 13:15:00Z],
- route: %Route{id: "r3", line: %Line{id: "l3"}, type: :ferry},
- stop: %Stop{id: "s3"},
- trip: %Trip{headsign: "other3", pattern_headsign: "h3"}
- }
- }
- ]
-
- expected_primary_sections = [
- %Screens.V2.WidgetInstance.Departures.NormalSection{
- header: %ScreensConfig.Departures.Header{
- arrow: nil,
- read_as: nil,
- subtitle: nil,
- title: nil
- },
- layout: %ScreensConfig.Departures.Layout{
- base: nil,
- include_later: false,
- max: nil,
- min: 1
- },
- grouping_type: :time,
- rows: expected_primary_departures
- }
- ]
-
- expected_secondary_sections = [
- %Screens.V2.WidgetInstance.Departures.NormalSection{
- header: %ScreensConfig.Departures.Header{
- arrow: nil,
- read_as: nil,
- subtitle: nil,
- title: nil
- },
- layout: %ScreensConfig.Departures.Layout{
- base: nil,
- include_later: false,
- max: nil,
- min: 1
- },
- grouping_type: :time,
- rows: expected_secondary_departures
- }
- ]
-
- config =
- @config
- |> put_primary_departures(primary_departures)
- |> put_secondary_departures_sections(secondary_departures)
-
- expect(@rds, :get, fn _primary_departures, @now ->
- [{:ok, [rds_countdown("s1", "l1", "other1", expected_primary_departures)]}]
- end)
-
- expect(@rds, :get, fn _secondary_departures, @now ->
- [{:ok, [rds_countdown("s3", "l3", "other3", expected_secondary_departures)]}]
- end)
-
- expected_instances =
- expected_departures_widget(config, expected_primary_sections, expected_secondary_sections)
-
- actual_instances = DupNew.Departures.instances(config, @now)
-
- assert actual_instances == expected_instances
- end
-
- test "handles bidirectional flag while creating departure sections" do
- primary_departures = [
- %Section{bidirectional: true, query: %Query{params: %Query.Params{stop_ids: ["s1"]}}}
- ]
-
- expected_departures = [
- %Departure{
- prediction: %Prediction{
- arrival_time: ~U[2024-10-11 12:27:00Z],
- departure_time: ~U[2024-10-11 12:30:00Z],
- route: %Route{id: "r1", line: %Line{id: "l1"}, type: :bus},
- stop: %Stop{id: "s1"},
- trip: %Trip{headsign: "other1", pattern_headsign: "h1", direction_id: 0}
- },
- schedule: nil
- },
- %Departure{
- prediction: %Prediction{
- arrival_time: ~U[2024-10-11 12:30:00Z],
- departure_time: ~U[2024-10-11 12:32:00Z],
- route: %Route{id: "r1", line: %Line{id: "l1"}, type: :bus},
- stop: %Stop{id: "s1"},
- trip: %Trip{headsign: "other3", pattern_headsign: "h3", direction_id: 1}
- },
- schedule: nil
- }
- ]
-
- all_departures = [
- %Departure{
- prediction: %Prediction{
- arrival_time: ~U[2024-10-11 12:27:00Z],
- departure_time: ~U[2024-10-11 12:30:00Z],
- route: %Route{id: "r1", line: %Line{id: "l1"}, type: :bus},
- stop: %Stop{id: "s1"},
- trip: %Trip{headsign: "other1", pattern_headsign: "h1", direction_id: 0}
- },
- schedule: nil
- },
- %Departure{
- prediction: %Prediction{
- arrival_time: ~U[2024-10-11 12:27:00Z],
- departure_time: ~U[2024-10-11 12:30:00Z],
- route: %Route{id: "r1", line: %Line{id: "l1"}, type: :bus},
- stop: %Stop{id: "s1"},
- trip: %Trip{headsign: "other1", pattern_headsign: "h1", direction_id: 0}
- },
- schedule: nil
- },
- %Departure{
- prediction: %Prediction{
- arrival_time: ~U[2024-10-11 12:27:00Z],
- departure_time: ~U[2024-10-11 12:30:00Z],
- route: %Route{id: "r1", line: %Line{id: "l1"}, type: :bus},
- stop: %Stop{id: "s1"},
- trip: %Trip{headsign: "other1", pattern_headsign: "h1", direction_id: 0}
- },
- schedule: nil
- },
- %Departure{
- prediction: %Prediction{
- arrival_time: ~U[2024-10-11 12:30:00Z],
- departure_time: ~U[2024-10-11 12:32:00Z],
- route: %Route{id: "r1", line: %Line{id: "l1"}, type: :bus},
- stop: %Stop{id: "s1"},
- trip: %Trip{headsign: "other3", pattern_headsign: "h3", direction_id: 1}
- },
- schedule: nil
- }
- ]
-
- expected_sections = [
- %Screens.V2.WidgetInstance.Departures.NormalSection{
- header: %ScreensConfig.Departures.Header{
- arrow: nil,
- read_as: nil,
- subtitle: nil,
- title: nil
- },
- layout: %ScreensConfig.Departures.Layout{
- base: nil,
- include_later: false,
- max: nil,
- min: 1
- },
- grouping_type: :time,
- rows: expected_departures
- }
- ]
-
- config =
- @config
- |> put_primary_departures(primary_departures)
-
- expect(@rds, :get, fn _primary_departures, @now ->
- [{:ok, [rds_countdown("s1", "l1", "other1", all_departures)]}]
- end)
-
- expect(@rds, :get, fn _secondary_departures, @now ->
- [{:ok, []}]
- end)
-
- expected_instances =
- expected_departures_widget(config, expected_sections, expected_sections)
-
- actual_instances = DupNew.Departures.instances(config, @now)
-
- assert actual_instances == expected_instances
- end
-
- test "creates NormalSections with First Departure rows for early morning scheduled departures" do
- primary_departures = [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s2"]}}}
- ]
-
- secondary_departures = [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s3"]}}},
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s4"]}}}
- ]
-
- expected_primary_schedule =
- %Schedule{
- arrival_time: ~U[2024-10-11 10:27:00Z],
- departure_time: ~U[2024-10-11 10:30:00Z],
- route: %Route{id: "r1", line: %Line{id: "l1"}, type: :subway},
- stop: %Stop{id: "s1"},
- trip: %Trip{headsign: "other1", pattern_headsign: "h1"}
- }
-
- expected_secondary_schedule =
- %Schedule{
- departure_time: ~U[2024-10-11 10:15:00Z],
- route: %Route{id: "r3", line: %Line{id: "l3"}, type: :subway},
- stop: %Stop{id: "s3"},
- trip: %Trip{headsign: "other3", pattern_headsign: "h3"}
- }
-
- expected_primary_departures = [
- {%Departure{prediction: nil, schedule: expected_primary_schedule}, :first_trip}
- ]
-
- expected_secondary_departures = [
- {%Departure{prediction: nil, schedule: expected_secondary_schedule}, :first_trip}
- ]
-
- expected_primary_sections = [
- %Screens.V2.WidgetInstance.Departures.NormalSection{
- header: %ScreensConfig.Departures.Header{
- arrow: nil,
- read_as: nil,
- subtitle: nil,
- title: nil
- },
- layout: %ScreensConfig.Departures.Layout{
- base: nil,
- include_later: false,
- max: nil,
- min: 1
- },
- grouping_type: :time,
- rows: expected_primary_departures
- }
- ]
-
- expected_secondary_sections = [
- %Screens.V2.WidgetInstance.Departures.NormalSection{
- header: %ScreensConfig.Departures.Header{
- arrow: nil,
- read_as: nil,
- subtitle: nil,
- title: nil
- },
- layout: %ScreensConfig.Departures.Layout{
- base: nil,
- include_later: false,
- max: nil,
- min: 1
- },
- grouping_type: :time,
- rows: expected_secondary_departures
- }
- ]
-
- config =
- @config
- |> put_primary_departures(primary_departures)
- |> put_secondary_departures_sections(secondary_departures)
-
- expect(@rds, :get, fn _primary_departures, @now ->
- [{:ok, [first_trip("s1", "l1", "other1", expected_primary_schedule)]}]
- end)
-
- expect(@rds, :get, fn _secondary_departures, @now ->
- [{:ok, [first_trip("s3", "l3", "other3", expected_secondary_schedule)]}]
- end)
-
- expected_instances =
- expected_departures_widget(config, expected_primary_sections, expected_secondary_sections)
-
- actual_instances = DupNew.Departures.instances(config, @now)
-
- assert actual_instances == expected_instances
- end
-
- test "creates NormalSections with Overnight Departure rows for service ended destinations" do
- now = ~U[2024-10-11 10:40:00Z]
-
- primary_departures = [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s3"]}}}
- ]
-
- expected_first_schedule =
- %Schedule{
- arrival_time: ~U[2024-10-11 10:27:00Z],
- departure_time: ~U[2024-10-11 10:30:00Z],
- route: %Route{id: "r1", line: %Line{id: "l1"}, type: :subway},
- stop: %Stop{id: "s1"},
- trip: %Trip{headsign: "other1", pattern_headsign: "h1"}
- }
-
- expected_last_schedule =
- %Schedule{
- departure_time: ~U[2024-10-11 10:50:00Z],
- route: %Route{id: "r3", line: %Line{id: "l3"}, type: :subway},
- stop: %Stop{id: "s3"},
- trip: %Trip{headsign: "other3", pattern_headsign: "h3"}
- }
-
- expected_departures = [
- {%Departure{prediction: nil, schedule: expected_first_schedule}, :first_trip},
- {%Departure{prediction: nil, schedule: expected_last_schedule}, :last_trip}
- ]
-
- expected_primary_sections = [
- %Screens.V2.WidgetInstance.Departures.NormalSection{
- header: %ScreensConfig.Departures.Header{
- arrow: nil,
- read_as: nil,
- subtitle: nil,
- title: nil
- },
- layout: %ScreensConfig.Departures.Layout{
- base: nil,
- include_later: false,
- max: nil,
- min: 1
- },
- grouping_type: :time,
- rows: expected_departures
- }
- ]
-
- config =
- @config
- |> put_primary_departures(primary_departures)
-
- expect(@rds, :get, fn _primary_departures, _now ->
- [
- {:ok,
- [
- service_ended("s1", "l1", "other1", expected_last_schedule),
- first_trip("s3", "l3", "other3", expected_first_schedule)
- ]}
- ]
- end)
-
- expect(@rds, :get, fn _primary_departures, _now -> [{:ok, []}] end)
-
- expected_instances =
- expected_departures_widget(
- config,
- expected_primary_sections,
- expected_primary_sections,
- now
- )
-
- actual_instances = DupNew.Departures.instances(config, now)
-
- assert actual_instances == expected_instances
- end
-
- test "creates OvernightSection for service ended in a section" do
- now = ~U[2024-10-11 10:40:00Z]
-
- primary_departures = [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s3"]}}}
- ]
-
- expected_first_schedule =
- %Schedule{
- arrival_time: ~U[2024-10-11 10:50:00Z],
- departure_time: ~U[2024-10-11 10:52:00Z],
- route: %Route{id: "r1", line: %Line{id: "l1"}, type: :subway},
- stop: %Stop{id: "s1"},
- trip: %Trip{headsign: "other1", pattern_headsign: "h1"}
- }
-
- expected_last_schedule =
- %Schedule{
- departure_time: ~U[2024-10-11 10:38:00Z],
- route: %Route{id: "r3", line: %Line{id: "l3"}, type: :subway},
- stop: %Stop{id: "s3"},
- trip: %Trip{headsign: "other3", pattern_headsign: "h3"}
- }
-
- expected_departures = [
- {%Departure{
- prediction: nil,
- schedule: expected_first_schedule
- }, :first_trip}
- ]
-
- expected_sections = [
- %Screens.V2.WidgetInstance.Departures.NormalSection{
- header: %ScreensConfig.Departures.Header{
- arrow: nil,
- read_as: nil,
- subtitle: nil,
- title: nil
- },
- layout: %ScreensConfig.Departures.Layout{
- base: nil,
- include_later: false,
- max: nil,
- min: 1
- },
- grouping_type: :time,
- rows: expected_departures
- },
- %Screens.V2.WidgetInstance.Departures.OvernightSection{
- routes: [
- %Screens.Routes.Route{id: "r3", type: :subway, line: %Screens.Lines.Line{id: "l3"}}
- ]
- }
- ]
-
- config =
- @config
- |> put_primary_departures(primary_departures)
-
- expect(@rds, :get, fn _primary_departures, _now ->
- [
- {:ok,
- [
- first_trip("s1", "l3", "other3", expected_first_schedule)
- ]},
- {:ok,
- [
- service_ended("s3", "l1", "other1", expected_last_schedule)
- ]}
- ]
- end)
-
- expect(@rds, :get, fn _secondary_departures, _now -> [{:ok, []}] end)
-
- expected_instances =
- expected_departures_widget(
- config,
- expected_sections,
- expected_sections,
- now
- )
-
- actual_instances = DupNew.Departures.instances(config, now)
-
- assert actual_instances == expected_instances
- end
-
- test "creates OvernightDepartures when all modes have service ended" do
- now = ~U[2024-10-11 10:40:00Z]
-
- primary_departures = [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s3"]}}}
- ]
-
- expected_last_schedule_one =
- %Schedule{
- arrival_time: ~U[2024-10-11 10:38:00Z],
- departure_time: ~U[2024-10-11 10:39:00Z],
- route: %Route{id: "r1", line: %Line{id: "l1"}, type: :subway},
- stop: %Stop{id: "s1"},
- trip: %Trip{headsign: "other1", pattern_headsign: "h1"}
- }
-
- expected_last_schedule_two =
- %Schedule{
- departure_time: ~U[2024-10-11 10:38:00Z],
- route: %Route{id: "r3", line: %Line{id: "l3"}, type: :subway},
- stop: %Stop{id: "s3"},
- trip: %Trip{headsign: "other3", pattern_headsign: "h3"}
- }
-
- config =
- @config
- |> put_primary_departures(primary_departures)
-
- expect(@rds, :get, fn _primary_departures, _now ->
- [
- {:ok,
- [
- service_ended("s1", "l1", "other1", expected_last_schedule_one)
- ]},
- {:ok,
- [
- service_ended("s3", "l3", "other3", expected_last_schedule_two)
- ]}
- ]
- end)
-
- expect(@rds, :get, fn _secondary_departures, _now -> [{:ok, []}] end)
-
- expected_instances = expected_overnight_departures_widget(config)
-
- actual_instances = DupNew.Departures.instances(config, now)
-
- assert actual_instances == expected_instances
- end
-
- test "creates HeadwaySections for destinations with headways" do
- primary_departures = [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s2"]}}}
- ]
-
- expected_route_id = "r1"
- expected_time_range = {6, 10}
- expected_headsign = "headsign"
-
- expected_primary_sections = [
- %Screens.V2.WidgetInstance.Departures.HeadwaySection{
- headsign: expected_headsign,
- route: expected_route_id,
- time_range: expected_time_range
- }
- ]
-
- config =
- @config
- |> put_primary_departures(primary_departures)
-
- expect(@rds, :get, fn _primary_departures, @now ->
- [
- {:ok,
- [
- headways(
- "s1",
- "l1",
- "headsign",
- expected_route_id,
- "Northbound",
- 0,
- expected_time_range
- ),
- headways(
- "s1",
- "l1",
- "headsign",
- expected_route_id,
- "Northbound",
- 0,
- expected_time_range
- )
- ]}
- ]
- end)
-
- expect(@rds, :get, fn _primary_departures, @now -> [{:ok, []}] end)
-
- expected_instances =
- expected_departures_widget(config, expected_primary_sections, expected_primary_sections)
-
- actual_instances = DupNew.Departures.instances(config, @now)
-
- assert actual_instances == expected_instances
- end
-
- test "creates HeadwaySections for destinations with headways with different headsigns" do
- primary_departures = [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s2"]}}}
- ]
-
- expected_route_id = "r1"
- expected_time_range = {6, 10}
- expected_direction_name = "Northbound"
-
- expected_primary_sections = [
- %Screens.V2.WidgetInstance.Departures.HeadwaySection{
- headsign: expected_direction_name,
- route: expected_route_id,
- time_range: expected_time_range
- }
- ]
-
- config =
- @config
- |> put_primary_departures(primary_departures)
-
- expect(@rds, :get, fn _primary_departures, @now ->
- [
- {:ok,
- [
- headways(
- "s1",
- "l1",
- "headsign",
- expected_route_id,
- expected_direction_name,
- 0,
- expected_time_range
- ),
- headways(
- "s1",
- "l1",
- "other_headsign",
- expected_route_id,
- expected_direction_name,
- 0,
- expected_time_range
- )
- ]}
- ]
- end)
-
- expect(@rds, :get, fn _primary_departures, @now -> [{:ok, []}] end)
-
- expected_instances =
- expected_departures_widget(config, expected_primary_sections, expected_primary_sections)
-
- actual_instances = DupNew.Departures.instances(config, @now)
-
- assert actual_instances == expected_instances
- end
-
- test "creates NormalSections for departures and headways" do
- expected_route_id = "r1"
- expected_time_range = {6, 10}
- expected_direction_name = "Northbound"
-
- primary_departures = [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s2"]}}}
- ]
-
- expected_primary_departure =
- %Departure{
- prediction: %Prediction{
- arrival_time: ~U[2024-10-11 12:27:00Z],
- departure_time: ~U[2024-10-11 12:30:00Z],
- route: %Route{id: "r1", line: %Line{id: "l1"}, type: :bus},
- stop: %Stop{id: "s1"},
- trip: %Trip{headsign: "other1", pattern_headsign: "h1", direction_id: 1}
- },
- schedule: nil
- }
-
- expected_primary_sections = [
- %Screens.V2.WidgetInstance.Departures.NormalSection{
- header: %ScreensConfig.Departures.Header{
- arrow: nil,
- read_as: nil,
- subtitle: nil,
- title: nil
- },
- layout: %ScreensConfig.Departures.Layout{
- base: nil,
- include_later: false,
- max: nil,
- min: 1
- },
- grouping_type: :time,
- rows: [
- expected_primary_departure,
- %HeadwayRow{
- id: "Test ID",
- line: %Line{id: "l1"},
- direction_id: 0,
- range: {6, 10},
- headsign: "other_headsign"
- }
- ]
- }
- ]
-
- config =
- @config
- |> put_primary_departures(primary_departures)
-
- expect(@rds, :get, fn _primary_departures, @now ->
- [
- {:ok,
- [
- rds_countdown("s1", "l1", "other1", [expected_primary_departure]),
- headways(
- "s1",
- "l1",
- "other_headsign",
- expected_route_id,
- expected_direction_name,
- 0,
- expected_time_range
- )
- ]}
- ]
- end)
-
- expect(@rds, :get, fn _secondary_departures, @now -> [{:ok, []}] end)
-
- expected_instances =
- expected_departures_widget(config, expected_primary_sections, expected_primary_sections)
-
- actual_instances = DupNew.Departures.instances(config, @now)
-
- assert actual_instances == expected_instances
- end
-
- test "creates NormalSections but removes headways when similar line/direction_id destinations in Countdown state" do
- expected_route_id = "r1"
- expected_time_range = {6, 10}
- expected_direction_name = "Northbound"
-
- primary_departures = [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s2"]}}}
- ]
-
- expected_primary_departure =
- %Departure{
- prediction: %Prediction{
- arrival_time: ~U[2024-10-11 12:27:00Z],
- departure_time: ~U[2024-10-11 12:30:00Z],
- route: %Route{id: "r1", line: %Line{id: "l1"}, type: :subway},
- stop: %Stop{id: "s1"},
- trip: %Trip{headsign: "other1", pattern_headsign: "h1", direction_id: 0}
- },
- schedule: nil
- }
-
- expected_primary_sections = [
- %Screens.V2.WidgetInstance.Departures.NormalSection{
- header: %ScreensConfig.Departures.Header{
- arrow: nil,
- read_as: nil,
- subtitle: nil,
- title: nil
- },
- layout: %ScreensConfig.Departures.Layout{
- base: nil,
- include_later: false,
- max: nil,
- min: 1
- },
- grouping_type: :time,
- rows: [
- expected_primary_departure
- ]
- }
- ]
-
- config =
- @config
- |> put_primary_departures(primary_departures)
-
- expect(@rds, :get, fn _primary_departures, @now ->
- [
- {:ok,
- [
- rds_countdown("s1", "l1", "other1", [expected_primary_departure]),
- headways(
- "s1",
- "l1",
- "other_headsign",
- expected_route_id,
- expected_direction_name,
- 0,
- expected_time_range
- )
- ]}
- ]
- end)
-
- expect(@rds, :get, fn _secondary_departures, @now -> [{:ok, []}] end)
-
- expected_instances =
- expected_departures_widget(config, expected_primary_sections, expected_primary_sections)
-
- actual_instances = DupNew.Departures.instances(config, @now)
-
- assert actual_instances == expected_instances
- end
-
- test "creates NormalSections but removes headways when similar line/direction_id destinations are in No Service state" do
- expected_route_id = "r1"
- expected_time_range = {6, 10}
- expected_direction_name = "Northbound"
-
- primary_departures = [
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s1"]}}},
- %Section{query: %Query{params: %Query.Params{stop_ids: ["s2"]}}}
- ]
-
- expected_primary_departure =
- %Departure{
- prediction: %Prediction{
- arrival_time: ~U[2024-10-11 12:27:00Z],
- departure_time: ~U[2024-10-11 12:30:00Z],
- route: %Route{id: "r1", line: %Line{id: "l1"}, type: :subway},
- stop: %Stop{id: "s1"},
- trip: %Trip{headsign: "other1", pattern_headsign: "h1", direction_id: 0}
- },
- schedule: nil
- }
-
- expected_primary_sections = [
- %Screens.V2.WidgetInstance.Departures.NormalSection{
- header: %ScreensConfig.Departures.Header{
- arrow: nil,
- read_as: nil,
- subtitle: nil,
- title: nil
- },
- layout: %ScreensConfig.Departures.Layout{
- base: nil,
- include_later: false,
- max: nil,
- min: 1
- },
- grouping_type: :time,
- rows: [
- expected_primary_departure
- ]
- }
- ]
-
- config =
- @config
- |> put_primary_departures(primary_departures)
-
- expect(@rds, :get, fn _primary_departures, @now ->
- [
- {:ok,
- [
- rds_countdown("s1", "l1", "other1", [expected_primary_departure]),
- no_service("s1", "l2", "some_headsign", [
- %Screens.Routes.Route{
- id: "r1",
- short_name: nil,
- long_name: nil,
- direction_names: nil,
- direction_destinations: nil,
- type: :bus,
- line: %Screens.Lines.Line{
- id: "l2",
- long_name: nil,
- short_name: nil,
- sort_order: nil
- }
- }
- ]),
- headways(
- "s1",
- "l2",
- "other_headsign",
- expected_route_id,
- expected_direction_name,
- 0,
- expected_time_range
- )
- ]}
- ]
- end)
-
- expect(@rds, :get, fn _secondary_departures, @now -> [{:ok, []}] end)
-
- expected_instances =
- expected_departures_widget(config, expected_primary_sections, expected_primary_sections)
-
- actual_instances = DupNew.Departures.instances(config, @now)
-
- assert actual_instances == expected_instances
- end
- end
-end
diff --git a/test/screens/v2/candidate_generator/dup_new_test.exs b/test/screens/v2/candidate_generator/dup_new_test.exs
deleted file mode 100644
index 6a1e25bad..000000000
--- a/test/screens/v2/candidate_generator/dup_new_test.exs
+++ /dev/null
@@ -1,81 +0,0 @@
-defmodule Screens.V2.CandidateGenerator.DupNewTest do
- use ExUnit.Case, async: true
-
- alias Screens.LocationContext
- alias Screens.Util.Assets
- alias Screens.V2.CandidateGenerator.DupNew
- alias Screens.V2.RDS
- alias Screens.V2.WidgetInstance.{DeparturesNoData, EvergreenContent}
- alias ScreensConfig.{Alerts, Departures, EvergreenContentItem, Header, Schedule}
- alias ScreensConfig.Screen
- alias ScreensConfig.Screen.Dup, as: DupConfig
-
- import Mox
- setup :verify_on_exit!
-
- import Screens.Inject
- @alert injected(Screens.Alerts.Alert)
- @stop injected(Screens.Stops.Stop)
- @location_context injected(LocationContext)
- @rds injected(RDS)
-
- setup do
- stub(@alert, :fetch, fn _ -> {:ok, []} end)
- stub(@stop, :fetch_stop_name, fn _ -> "" end)
-
- stub(@location_context, :fetch, fn _, _, _ ->
- {:ok, %LocationContext{home_stop: "Test", routes: []}}
- end)
-
- stub(@rds, :get, fn _, _ -> [{:ok, []}] end)
- :ok
- end
-
- describe "candidate_instances/2" do
- @config %Screen{
- app_id: :dup_v2,
- app_params: %DupConfig{
- alerts: %Alerts{stop_id: "place-abcde"},
- header: %Header.StopName{stop_name: "Test Stop"},
- primary_departures: %Departures{sections: []},
- secondary_departures: %Departures{sections: []}
- },
- vendor: :outfront,
- device_id: "TEST",
- name: "TEST"
- }
- @now ~U[2024-01-15 11:45:30Z]
-
- test "returns evergreen content when scheduled" do
- schedule = %Schedule{start_dt: ~U[2024-01-01 00:00:00Z], end_dt: ~U[2024-02-01 00:00:00Z]}
-
- item = %EvergreenContentItem{
- asset_path: "test.png",
- priority: [1],
- schedule: [schedule],
- slot_names: ["bottom_pane_zero"]
- }
-
- config = put_in(@config.app_params.evergreen_content, [item])
- now_active = ~U[2024-01-10 00:00:00Z]
- now_inactive = ~U[2024-02-02 00:00:00Z]
-
- expected_instance = %EvergreenContent{
- screen: config,
- asset_url: Assets.s3_asset_url("test.png"),
- now: now_active,
- priority: [1],
- schedule: [schedule],
- slot_names: [:bottom_pane_zero]
- }
-
- assert expected_instance in DupNew.candidate_instances(config, now_active)
- assert expected_instance not in DupNew.candidate_instances(config, now_inactive)
- end
-
- test "stub: always returns no-data state for departures" do
- expected_instance = %DeparturesNoData{screen: @config, slot_name: :main_content_zero}
- assert expected_instance in DupNew.candidate_instances(@config, @now)
- end
- end
-end
diff --git a/test/screens/v2/candidate_generator/dup_test.exs b/test/screens/v2/candidate_generator/dup_test.exs
index 4a035c078..6dbea84d8 100644
--- a/test/screens/v2/candidate_generator/dup_test.exs
+++ b/test/screens/v2/candidate_generator/dup_test.exs
@@ -1,96 +1,81 @@
defmodule Screens.V2.CandidateGenerator.DupTest do
use ExUnit.Case, async: true
+ alias Screens.LocationContext
+ alias Screens.Util.Assets
alias Screens.V2.CandidateGenerator.Dup
- alias ScreensConfig.{Alerts, Departures, Header}
+ alias Screens.V2.RDS
+ alias Screens.V2.WidgetInstance.{DeparturesNoData, EvergreenContent}
+ alias ScreensConfig.{Alerts, Departures, EvergreenContentItem, Header, Schedule}
alias ScreensConfig.Screen
alias ScreensConfig.Screen.Dup, as: DupConfig
+ import Mox
+ setup :verify_on_exit!
+
+ import Screens.Inject
+ @alert injected(Screens.Alerts.Alert)
+ @stop injected(Screens.Stops.Stop)
+ @location_context injected(LocationContext)
+ @rds injected(RDS)
+
setup do
- config = %Screen{
+ stub(@alert, :fetch, fn _ -> {:ok, []} end)
+ stub(@stop, :fetch_stop_name, fn _ -> "" end)
+
+ stub(@location_context, :fetch, fn _, _, _ ->
+ {:ok, %LocationContext{home_stop: "Test", routes: []}}
+ end)
+
+ stub(@rds, :get, fn _, _ -> [{:ok, []}] end)
+ :ok
+ end
+
+ describe "candidate_instances/2" do
+ @config %Screen{
+ app_id: :dup_v2,
app_params: %DupConfig{
- header: %Header.StopId{stop_id: "place-gover"},
- primary_departures: struct(Departures),
- secondary_departures: struct(Departures),
- alerts: struct(Alerts)
+ alerts: %Alerts{stop_id: "place-abcde"},
+ header: %Header.StopName{stop_name: "Test Stop"},
+ primary_departures: %Departures{sections: []},
+ secondary_departures: %Departures{sections: []}
},
vendor: :outfront,
device_id: "TEST",
- name: "TEST",
- app_id: :dup_v2
+ name: "TEST"
}
+ @now ~U[2024-01-15 11:45:30Z]
- %{config: config}
- end
+ test "returns evergreen content when scheduled" do
+ schedule = %Schedule{start_dt: ~U[2024-01-01 00:00:00Z], end_dt: ~U[2024-02-01 00:00:00Z]}
- describe "screen_template/1" do
- test "returns template", %{config: config} do
- assert {:screen,
- %{
- screen_normal: [
- {:rotation_zero,
- %{
- rotation_normal_zero: [
- :header_zero,
- {:body_zero,
- %{
- body_normal_zero: [:main_content_zero],
- body_split_zero: [:main_content_reduced_zero, :bottom_pane_zero]
- }}
- ],
- rotation_takeover_zero: [:full_rotation_zero]
- }},
- {:rotation_one,
- %{
- rotation_normal_one: [
- :header_one,
- {:body_one,
- %{
- body_normal_one: [:main_content_one],
- body_split_one: [:main_content_reduced_one, :bottom_pane_one]
- }}
- ],
- rotation_takeover_one: [:full_rotation_one]
- }},
- {:rotation_two,
- %{
- rotation_normal_two: [
- :header_two,
- {:body_two,
- %{
- body_normal_two: [:main_content_two],
- body_split_two: [:main_content_reduced_two, :bottom_pane_two]
- }}
- ],
- rotation_takeover_two: [:full_rotation_two]
- }}
- ]
- }} == Dup.screen_template(config)
- end
- end
+ item = %EvergreenContentItem{
+ asset_path: "test.png",
+ priority: [1],
+ schedule: [schedule],
+ slot_names: ["bottom_pane_zero"]
+ }
- describe "candidate_instances/6" do
- test "returns expected instances", %{config: config} do
- now = ~U[2020-04-06T10:00:00Z]
- header_instances_fn = fn ^config, ^now -> [:header] end
- departures_instances_fn = fn ^config, ^now -> [:departures] end
- evergreen_content_instances_fn = fn ^config, ^now -> [:evergreen] end
- alerts_instances_fn = fn ^config, ^now -> [:alert] end
- emergency_takeover_instances_fn = fn ^config, ^now -> [:emergency_takeover] end
+ config = put_in(@config.app_params.evergreen_content, [item])
+ now_active = ~U[2024-01-10 00:00:00Z]
+ now_inactive = ~U[2024-02-02 00:00:00Z]
- actual_instances =
- Dup.candidate_instances(
- config,
- now,
- header_instances_fn,
- evergreen_content_instances_fn,
- departures_instances_fn,
- alerts_instances_fn,
- emergency_takeover_instances_fn
- )
+ expected_instance = %EvergreenContent{
+ screen: config,
+ asset_url: Assets.s3_asset_url("test.png"),
+ now: now_active,
+ priority: [1],
+ schedule: [schedule],
+ slot_names: [:bottom_pane_zero]
+ }
+
+ assert expected_instance in Dup.candidate_instances(config, now_active)
+ assert expected_instance not in Dup.candidate_instances(config, now_inactive)
+ end
- assert Enum.sort(actual_instances) ==
- ~w[alert departures emergency_takeover evergreen header]a
+ test "stub: always returns no-data state for departures" do
+ expected_instance = %DeparturesNoData{screen: @config, slot_name: :main_content_zero}
+ assert expected_instance in Dup.candidate_instances(@config, @now)
end
end
end
diff --git a/test/screens/v2/candidate_generator/widgets/departures_test.exs b/test/screens/v2/candidate_generator/widgets/departures_test.exs
index b2b10c76d..ac86a4e47 100644
--- a/test/screens/v2/candidate_generator/widgets/departures_test.exs
+++ b/test/screens/v2/candidate_generator/widgets/departures_test.exs
@@ -73,7 +73,7 @@ defmodule Screens.V2.CandidateGenerator.Widgets.DeparturesTest do
defp departures_instances(config, options) do
Departures.departures_instances(
config,
- nil,
+ Keyword.get(options, :now),
Keyword.merge(
[
departure_fetch_fn: fn _, _ -> :error end,
@@ -385,16 +385,17 @@ defmodule Screens.V2.CandidateGenerator.Widgets.DeparturesTest do
route_fetch_fn: route_fetch_fn
)
end
- end
- describe "fetch_section_departures/1" do
test "filters departures by time when a section has a max_minutes" do
now = ~U[2024-01-01 12:00:00Z]
- section = %Section{
- query: %Query{params: %Query.Params{stop_ids: ["S"]}},
- filters: %Filters{max_minutes: 60}
- }
+ config =
+ build_config([
+ %Section{
+ query: %Query{params: %Query.Params{stop_ids: ["S"]}},
+ filters: %Filters{max_minutes: 60}
+ }
+ ])
included_departures = [
build_departure("1", 0, nil, DateTime.add(now, 59, :minute)),
@@ -406,23 +407,26 @@ defmodule Screens.V2.CandidateGenerator.Widgets.DeparturesTest do
[build_departure("1", 0, nil, DateTime.add(now, 61, :minute)) | included_departures]}
end
- assert {:ok, included_departures} ==
- Departures.fetch_section_departures(section, [], fetch_fn, now)
+ assert [%DeparturesWidget{sections: [%NormalSection{rows: ^included_departures}]}] =
+ departures_instances(config, departure_fetch_fn: fetch_fn, now: now)
end
test "filters departures with included route-directions" do
- section = %Section{
- query: %Query{params: %Query.Params{stop_ids: ["S"]}},
- filters: %Filters{
- route_directions: %RouteDirections{
- action: :include,
- targets: [
- %RouteDirection{route_id: "39", direction_id: 0},
- %RouteDirection{route_id: "41", direction_id: 0}
- ]
+ config =
+ build_config([
+ %Section{
+ query: %Query{params: %Query.Params{stop_ids: ["S"]}},
+ filters: %Filters{
+ route_directions: %RouteDirections{
+ action: :include,
+ targets: [
+ %RouteDirection{route_id: "39", direction_id: 0},
+ %RouteDirection{route_id: "41", direction_id: 0}
+ ]
+ }
+ }
}
- }
- }
+ ])
included_departure = build_departure("41", 0)
@@ -430,23 +434,26 @@ defmodule Screens.V2.CandidateGenerator.Widgets.DeparturesTest do
{:ok, [build_departure("41", 1), included_departure, build_departure("1", 1)]}
end
- assert {:ok, [included_departure]} ==
- Departures.fetch_section_departures(section, [], fetch_fn)
+ assert [%DeparturesWidget{sections: [%NormalSection{rows: [^included_departure]}]}] =
+ departures_instances(config, departure_fetch_fn: fetch_fn)
end
test "rejects departures with excluded route-directions" do
- section = %Section{
- query: %Query{params: %Query.Params{stop_ids: ["S"]}},
- filters: %Filters{
- route_directions: %RouteDirections{
- action: :exclude,
- targets: [
- %RouteDirection{route_id: "39", direction_id: 0},
- %RouteDirection{route_id: "41", direction_id: 0}
- ]
+ config =
+ build_config([
+ %Section{
+ query: %Query{params: %Query.Params{stop_ids: ["S"]}},
+ filters: %Filters{
+ route_directions: %RouteDirections{
+ action: :exclude,
+ targets: [
+ %RouteDirection{route_id: "39", direction_id: 0},
+ %RouteDirection{route_id: "41", direction_id: 0}
+ ]
+ }
+ }
}
- }
- }
+ ])
included_departures = [build_departure("41", 1), build_departure("1", 1)]
@@ -454,8 +461,8 @@ defmodule Screens.V2.CandidateGenerator.Widgets.DeparturesTest do
{:ok, [build_departure("41", 0) | included_departures]}
end
- assert {:ok, included_departures} ==
- Departures.fetch_section_departures(section, [], fetch_fn)
+ assert [%DeparturesWidget{sections: [%NormalSection{rows: ^included_departures}]}] =
+ departures_instances(config, departure_fetch_fn: fetch_fn)
end
test "filters departures for sections configured as bidirectional" do
diff --git a/test/screens/v2/departure_test.exs b/test/screens/v2/departure_test.exs
index a26cb7d91..2257924f0 100644
--- a/test/screens/v2/departure_test.exs
+++ b/test/screens/v2/departure_test.exs
@@ -505,7 +505,7 @@ defmodule Screens.V2.DepartureTest do
opts = build_opts(schedule_route_type_filter: [:rail, :ferry])
assert Departure.fetch_schedules(params, opts) ==
- {:test, %{route_type: [:rail, :ferry], stop_ids: ["place-sstat"]}}
+ {:test, %{route_type: [:ferry, :rail], stop_ids: ["place-sstat"]}}
end
test "fetches with the intersection of params route_type and opts route_type" do
@@ -529,9 +529,8 @@ defmodule Screens.V2.DepartureTest do
assert Departure.fetch_schedules(params, opts) == {:ok, []}
end
- test "defaults to all route types when route_type is not in params or opts" do
- assert Departure.fetch_schedules(%{}, build_opts()) ==
- {:test, %{route_type: [:light_rail, :subway, :rail, :bus, :ferry]}}
+ test "defaults to no route type filter when route_type is not in params or opts" do
+ assert Departure.fetch_schedules(%{}, build_opts()) == {:test, %{}}
end
end
end
diff --git a/test/screens/v2/rds_test.exs b/test/screens/v2/rds_test.exs
index 8c5502ea8..5d19b4ba9 100644
--- a/test/screens/v2/rds_test.exs
+++ b/test/screens/v2/rds_test.exs
@@ -1,6 +1,5 @@
defmodule Screens.V2.RDSTest do
use ExUnit.Case, async: true
- import ExUnit.CaptureLog
alias Screens.Alerts.Alert
alias Screens.Config.Cache
@@ -98,9 +97,7 @@ defmodule Screens.V2.RDSTest do
stop: %Stop{id: stop_id},
line: %Line{id: line_id},
headsign: headsign,
- state: %RDS.FirstTrip{
- first_scheduled_departure: %Departure{prediction: nil, schedule: schedule}
- }
+ state: %RDS.FirstTrip{first_schedule: schedule}
}
end
@@ -109,9 +106,7 @@ defmodule Screens.V2.RDSTest do
stop: %Stop{id: stop_id},
line: %Line{id: line_id},
headsign: headsign,
- state: %RDS.ServiceEnded{
- last_scheduled_departure: %Departure{prediction: nil, schedule: schedule}
- }
+ state: %RDS.ServiceEnded{last_schedule: schedule}
}
end
@@ -121,15 +116,13 @@ defmodule Screens.V2.RDSTest do
headsign,
route_id,
direction_name,
- direction_id,
- departure_id
+ direction_id
) do
%RDS{
stop: %Stop{id: stop_id},
line: %Line{id: line_id},
headsign: headsign,
state: %RDS.Headways{
- departure_id: departure_id,
route_id: route_id,
direction_name: direction_name,
direction_id: direction_id,
@@ -238,7 +231,7 @@ defmodule Screens.V2.RDSTest do
]
expect(@departure, :fetch, fn
- %{direction_id: 0, route_type: :bus, stop_ids: ["s0"]}, [now: ^now] ->
+ %{direction_id: 0, route_type: :bus, stop_ids: ["s0"]}, [{:now, ^now} | _] ->
{
:ok,
expected_departures_one ++
@@ -299,7 +292,7 @@ defmodule Screens.V2.RDSTest do
]
expect(@departure, :fetch, fn
- %{direction_id: 0, route_type: :bus, stop_ids: ^stop_ids}, [now: ^now] ->
+ %{direction_id: 0, route_type: :bus, stop_ids: ^stop_ids}, [{:now, ^now} | _] ->
{
:ok,
expected_departures
@@ -550,7 +543,7 @@ defmodule Screens.V2.RDSTest do
stub(@headways, :get, fn _, _ -> {5, 10} end)
expect(@departure, :fetch, fn
- %{direction_id: :both, route_type: :bus, stop_ids: ^stop_ids}, [now: ^now] ->
+ %{direction_id: :both, route_type: :bus, stop_ids: ^stop_ids}, [{:now, ^now} | _] ->
{:ok, []}
end)
@@ -601,7 +594,7 @@ defmodule Screens.V2.RDSTest do
}
expect(@departure, :fetch, fn
- %{direction_id: :both, route_type: :bus, stop_ids: ^stop_ids}, [now: ^now] ->
+ %{direction_id: :both, route_type: :bus, stop_ids: ^stop_ids}, [{:now, ^now} | _] ->
{:ok, []}
end)
@@ -745,9 +738,9 @@ defmodule Screens.V2.RDSTest do
assert RDS.get(departures, now) == [
{:ok,
[
- headways("sA", "l1", "hA", "r1", "Northbound", 0, "Test ID 11"),
- headways("sB", "l2", "hB", "r2", "Westbound", 1, "Test ID 21"),
- headways("sC", "l2", "hC", "r2", "Eastbound", 0, "Test ID 31")
+ headways("sA", "l1", "hA", "r1", "Northbound", 0),
+ headways("sB", "l2", "hB", "r2", "Westbound", 1),
+ headways("sC", "l2", "hC", "r2", "Eastbound", 0)
]}
]
end
@@ -780,12 +773,7 @@ defmodule Screens.V2.RDSTest do
}
assert RDS.get(departures) == [
- {:ok,
- [
- countdowns("sA", "l1", "hA", []),
- no_service("sB", "l2", "hB"),
- no_service("sC", "l2", "hC")
- ]}
+ {:ok, [no_service("sB", "l2", "hB"), no_service("sC", "l2", "hC")]}
]
end
@@ -816,12 +804,7 @@ defmodule Screens.V2.RDSTest do
}
assert RDS.get(departures) == [
- {:ok,
- [
- countdowns("sA", "l1", "hA", []),
- no_service("sB", "l2", "hB"),
- no_service("sC", "l2", "hC")
- ]}
+ {:ok, [no_service("sB", "l2", "hB"), no_service("sC", "l2", "hC")]}
]
end
@@ -878,12 +861,7 @@ defmodule Screens.V2.RDSTest do
}
assert RDS.get(departures) == [
- {:ok,
- [
- no_service("sA", "l1", "hA"),
- countdowns("sB", "l2", "hB", []),
- no_service("sC", "l2", "hC")
- ]}
+ {:ok, [no_service("sA", "l1", "hA"), no_service("sC", "l2", "hC")]}
]
end
@@ -914,14 +892,7 @@ defmodule Screens.V2.RDSTest do
}
# All destinations are affected since the alert targets the entire bus route type
- assert RDS.get(departures) == [
- {:ok,
- [
- countdowns("sA", "l1", "hA", []),
- countdowns("sB", "l2", "hB", []),
- countdowns("sC", "l2", "hC", [])
- ]}
- ]
+ assert RDS.get(departures) == [{:ok, []}]
end
test "creates NoService for destinations not affected by alerts" do
@@ -962,8 +933,8 @@ defmodule Screens.V2.RDSTest do
end
end
- describe "get/1 error for state" do
- test "logs and returns nil for fallback creating state" do
+ describe "get/1 no state" do
+ test "returns nothing when no state applies" do
now = ~U[2024-10-11 11:44:00Z]
stop_ids = ~w[s0 s1]
@@ -1069,10 +1040,8 @@ defmodule Screens.V2.RDSTest do
expect(@schedule, :fetch, fn %{stop_ids: ^stop_ids}, _now -> {:ok, all_schedules} end)
expect_standard_stations(stop_ids)
expect_standard_route_patterns(stop_ids)
- {result, log} = with_log(fn -> RDS.get(departures, now) end)
- assert result == [{:ok, []}]
- assert log =~ "rds_state_creation_failed"
+ assert RDS.get(departures, now) == [{:ok, []}]
end
end
@@ -1111,7 +1080,7 @@ defmodule Screens.V2.RDSTest do
now = ~U[2024-10-11 12:00:00Z]
stop_ids = ~w[s0]
- expect(@departure, :fetch, fn %{stop_ids: ^stop_ids}, [now: ^now] ->
+ expect(@departure, :fetch, fn %{stop_ids: ^stop_ids}, [{:now, ^now} | _] ->
:error
end)
@@ -1142,7 +1111,7 @@ defmodule Screens.V2.RDSTest do
}
]
- stub(@departure, :fetch, fn %{stop_ids: stop_ids}, [now: ^now] ->
+ stub(@departure, :fetch, fn %{stop_ids: stop_ids}, [{:now, ^now} | _] ->
case stop_ids do
^stop_ids_primary -> {:ok, expected_departures}
^stop_ids_secondary -> :error
@@ -1191,10 +1160,10 @@ defmodule Screens.V2.RDSTest do
]
stub(@departure, :fetch, fn
- %{direction_id: 0, route_type: :bus, stop_ids: ^stop_ids}, [now: ^now] ->
+ %{direction_id: 0, route_type: :bus, stop_ids: ^stop_ids}, [{:now, ^now} | _] ->
{:ok, bus_departures}
- %{direction_id: 0, route_type: :subway, stop_ids: ^stop_ids}, [now: ^now] ->
+ %{direction_id: 0, route_type: :subway, stop_ids: ^stop_ids}, [{:now, ^now} | _] ->
{:ok, subway_departures}
end)
diff --git a/test/screens/v2/widget_instance/departures_test.exs b/test/screens/v2/widget_instance/departures_test.exs
index 839d1e003..bf9b7226d 100644
--- a/test/screens/v2/widget_instance/departures_test.exs
+++ b/test/screens/v2/widget_instance/departures_test.exs
@@ -111,44 +111,6 @@ defmodule Screens.V2.WidgetInstance.DeparturesTest do
}
end
- test "returns serialized normal_section for section with row with no scheduled time", %{
- dup_screen: dup_screen,
- now: now
- } do
- section = %NormalSection{
- layout: %Layout{},
- header: %Header{},
- rows: [
- %Departure{
- schedule:
- struct(Schedule,
- arrival_time: nil,
- departure_time: nil,
- route: route(id: "Orange", type: :subway, name: "Orange Line"),
- stop: %Stop{id: "70015", name: "Back Bay"},
- stop_headsign: "Oak Grove"
- )
- }
- ]
- }
-
- assert %{
- rows: [
- %{
- headsign: %{headsign: "Oak Grove"},
- # MD5 hash when the schedule ID is nil. Won't change unless ID does.
- id: "1B2M2Y8AsgTpgAmY7PhCfg==",
- route: %{color: :orange, text: "OL", type: :text},
- times_with_crowding: [%{id: nil, crowding: nil, time: %{type: :overnight}}],
- type: :departure_row
- }
- ],
- type: :normal_section,
- grouping_type: :time
- } =
- Departures.serialize_section(section, dup_screen, now, true)
- end
-
test "returns serialized headway_section for one configured section", %{
dup_screen: dup_screen,
now: now
@@ -383,7 +345,7 @@ defmodule Screens.V2.WidgetInstance.DeparturesTest do
line: %Line{id: "line-Green"},
direction_id: 0,
range: {20, 30},
- headsign: %{headsign: "Westbound"}
+ headsign: "Westbound"
}
]
@@ -399,31 +361,24 @@ defmodule Screens.V2.WidgetInstance.DeparturesTest do
rows: [
%{headsign: %{headsign: "Medford/Tufts"}},
%{
- headsign: %{headsign: %{headsign: "Westbound"}},
+ headsign: %{headsign: "Westbound"},
direction_id: 0,
route: %{type: :text, text: "GL", color: :green},
- times_with_crowding: [
- %{id: "Test ID", time: %{type: :status, pages: ["every 20-30m"]}}
- ]
+ times_with_crowding: [%{time: %{type: :status, pages: ["every 20-30m"]}}]
}
]
} =
Departures.serialize_section(section, dup_screen, now, false)
end
- test "serializes normal section with first trip row", %{
- dup_screen: dup_screen,
- now: now
- } do
+ test "serializes normal section with first trip row", %{dup_screen: dup_screen, now: now} do
rows = [
- {%Departure{
- schedule: %Schedule{
- id: "one",
- departure_time: ~U[2020-01-01T00:01:10Z],
- route: route(id: "Green-E", type: :subway),
- trip: %Trip{headsign: "Medford/Tufts", direction_id: 1},
- stop: %Stop{}
- }
+ {%Schedule{
+ id: "one",
+ departure_time: ~U[2020-01-01T00:01:10Z],
+ route: route(id: "Green-E", type: :subway),
+ trip: %Trip{headsign: "Medford/Tufts", direction_id: 1},
+ stop: %Stop{}
}, :first_trip}
]
@@ -453,20 +408,15 @@ defmodule Screens.V2.WidgetInstance.DeparturesTest do
Departures.serialize_section(section, dup_screen, now, false)
end
- test "serializes normal section with last trip row", %{
- dup_screen: dup_screen,
- now: now
- } do
+ test "serializes normal section with service ended row", %{dup_screen: dup_screen, now: now} do
rows = [
- {%Departure{
- schedule: %Schedule{
- id: "one",
- departure_time: ~U[2020-01-01T00:01:10Z],
- route: route(id: "Green-E", type: :subway),
- trip: %Trip{headsign: "Medford/Tufts", direction_id: 1},
- stop: %Stop{}
- }
- }, :last_trip}
+ {%Schedule{
+ id: "one",
+ departure_time: ~U[2020-01-01T00:01:10Z],
+ route: route(id: "Green-E", type: :subway),
+ trip: %Trip{headsign: "Medford/Tufts", direction_id: 1},
+ stop: %Stop{}
+ }, :service_ended}
]
section = %NormalSection{
@@ -1197,7 +1147,8 @@ defmodule Screens.V2.WidgetInstance.DeparturesTest do
Departures.serialize_times_with_crowding([departure], screen, now)
end
- test "includes schedule for rail when appropriate", %{bus_shelter_screen: screen} do
+ test "includes originally scheduled time on DUPs when it differs from the predicted time",
+ %{bus_shelter_screen: bus_shelter_screen, dup_screen: dup_screen} do
now = ~U[2020-01-01T00:00:00Z]
departure = %Departure{
@@ -1222,23 +1173,26 @@ defmodule Screens.V2.WidgetInstance.DeparturesTest do
time: %{hour: 9, minute: 20, type: :timestamp},
scheduled_time: %{hour: 9, minute: 15, type: :timestamp}
}
- ] =
- Departures.serialize_times_with_crowding([departure], screen, now)
+ ] = Departures.serialize_times_with_crowding([departure], dup_screen, now)
+
+ assert [%{scheduled_time: nil}] =
+ Departures.serialize_times_with_crowding([departure], bus_shelter_screen, now)
end
- test "doesn't include schedule when the same", %{bus_shelter_screen: screen} do
+ test "omits scheduled time when it serializes the same as predicted", %{dup_screen: screen} do
now = ~U[2020-01-01T00:00:00Z]
departure = %Departure{
prediction: %Prediction{
- arrival_time: ~U[2020-01-01T02:20:00Z],
- departure_time: ~U[2020-01-01T02:20:00Z],
+ arrival_time: ~U[2020-01-01T02:20:01Z],
+ departure_time: ~U[2020-01-01T02:20:01Z],
route: %Route{type: :rail},
stop: %Stop{}
},
schedule: %Schedule{
- arrival_time: ~U[2020-01-01T02:20:00Z],
- departure_time: ~U[2020-01-01T02:20:00Z],
+ # one second different, so hh:mm serialization is the same
+ arrival_time: ~U[2020-01-01T02:20:02Z],
+ departure_time: ~U[2020-01-01T02:20:02Z],
route: %Route{type: :rail},
stop: %Stop{}
}
@@ -1248,29 +1202,7 @@ defmodule Screens.V2.WidgetInstance.DeparturesTest do
Departures.serialize_times_with_crowding([departure], screen, now)
end
- test "doesn't include schedule when not rail", %{bus_shelter_screen: screen} do
- now = ~U[2020-01-01T00:00:00Z]
-
- departure = %Departure{
- prediction: %Prediction{
- arrival_time: ~U[2020-01-01T02:20:00Z],
- departure_time: ~U[2020-01-01T02:20:00Z],
- route: %Route{type: :bus},
- stop: %Stop{}
- },
- schedule: %Schedule{
- arrival_time: ~U[2020-01-01T02:15:00Z],
- departure_time: ~U[2020-01-01T02:15:00Z],
- route: %Route{type: :bus},
- stop: %Stop{}
- }
- }
-
- [result] = Departures.serialize_times_with_crowding([departure], screen, now)
- assert is_nil(Map.get(result, :scheduled_time))
- end
-
- test "doesn't include schedule when prediction is ARR/BRD", %{bus_shelter_screen: screen} do
+ test "omits scheduled time when predicted time is not a timestamp", %{dup_screen: screen} do
now = ~U[2020-01-01T00:00:00Z]
departure = %Departure{
@@ -1292,7 +1224,7 @@ defmodule Screens.V2.WidgetInstance.DeparturesTest do
Departures.serialize_times_with_crowding([departure], screen, now)
end
- test "doesn't include schedule when schedule is nil", %{bus_shelter_screen: screen} do
+ test "omits scheduled time when departure has no schedule", %{dup_screen: screen} do
now = ~U[2020-01-01T00:00:00Z]
departure = %Departure{
@@ -1304,8 +1236,8 @@ defmodule Screens.V2.WidgetInstance.DeparturesTest do
}
}
- [result] = Departures.serialize_times_with_crowding([departure], screen, now)
- assert is_nil(Map.get(result, :scheduled_time))
+ assert [%{scheduled_time: nil}] =
+ Departures.serialize_times_with_crowding([departure], screen, now)
end
test "serializes stops away status when provided", %{dup_screen: screen} do