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