From a690232c3f4e5044eb86c1c7666e5ded4c73162b Mon Sep 17 00:00:00 2001 From: Josh Larson Date: Mon, 18 Nov 2024 16:11:01 -0500 Subject: [PATCH] feat: Add a nicer summary view for trip planner output (#2204) --------- Co-authored-by: Cristen Jones --- assets/tailwind.config.js | 38 +++- config/config.exs | 2 + lib/dotcom/trip_plan/itinerary_group.ex | 69 -------- lib/dotcom/trip_plan/itinerary_groups.ex | 161 +++++++++++++++++ lib/dotcom_web.ex | 2 +- lib/dotcom_web/components/components.ex | 5 +- .../live_components/trip_planner_form.ex | 7 +- lib/dotcom_web/components/route_symbols.ex | 167 ++++++++++++++++++ .../trip_planner/itinerary_detail.ex | 41 +++++ .../trip_planner/itinerary_group.ex | 155 +++++++++++++--- lib/dotcom_web/components/trip_planner/leg.ex | 31 ++-- lib/dotcom_web/live/trip_planner.ex | 31 ++-- .../alert/{show.html.eex => show.html.heex} | 30 ++-- lib/dotcom_web/views/alert_view.ex | 10 -- lib/dotcom_web/views/helpers.ex | 9 +- lib/routes/route.ex | 7 + mix.exs | 3 +- mix.lock | 3 +- .../icon-svg/icon-mode-shuttle-default.svg | 1 + .../components/route_symbols_test.exs | 113 ++++++++++++ .../controllers/alert_controller_test.exs | 10 +- test/dotcom_web/views/alert_view_test.exs | 34 ---- test/support/factories/routes/route.ex | 42 +++++ 23 files changed, 764 insertions(+), 207 deletions(-) delete mode 100644 lib/dotcom/trip_plan/itinerary_group.ex create mode 100644 lib/dotcom/trip_plan/itinerary_groups.ex create mode 100644 lib/dotcom_web/components/route_symbols.ex create mode 100644 lib/dotcom_web/components/trip_planner/itinerary_detail.ex rename lib/dotcom_web/templates/alert/{show.html.eex => show.html.heex} (73%) create mode 100644 priv/static/icon-svg/icon-mode-shuttle-default.svg create mode 100644 test/dotcom_web/components/route_symbols_test.exs diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js index 4bcf98b647..be9b51dea7 100644 --- a/assets/tailwind.config.js +++ b/assets/tailwind.config.js @@ -16,7 +16,6 @@ module.exports = { preflight: false }, blocklist: ["container", "collapse"], - important: true, content: [ ...content, "./js/**/*.js", @@ -25,11 +24,44 @@ module.exports = { "../lib/dotcom_web.ex", "../lib/dotcom_web/**/*.*ex" ], - safelist: [...safelist], + safelist: [ + ...safelist, + { + pattern: /(bg|text|border|ring)-(logan-express|blue|green|orange|red|silver|bus|ferry|)./ + } + ], theme: { extend: { colors: { - ...colors + ...colors, + gray: { + DEFAULT: "#494f5c", + dark: "#1c1e23", + light: "#788093", + lighter: "#b0b5c0" + }, + "brand-primary": { + DEFAULT: "#165c96", + darkest: "#0b2f4c" + }, + "logan-express": { + BB: "#f16823", + BT: "#0066cc", + DV: "#704c9f", + FH: "#e81d2d", + WO: "#00954c" + }, + massport: "#104c8f", + subway: "#494f5c", + // These will come from the design system someday + "blue-line": "#003da5", + "green-line": "#00843d", + "orange-line": "#ed8b00", + "red-line": "#da291c", + "silver-line": "#7c878e", + "commuter-rail": "#80276c", + bus: "#ffc72c", + ferry: "#008eaa" } }, fontFamily: { diff --git a/config/config.exs b/config/config.exs index 54b74281f4..98cae89def 100644 --- a/config/config.exs +++ b/config/config.exs @@ -35,6 +35,8 @@ config :sentry, root_source_code_paths: [File.cwd!()], context_lines: 5 +config :mbta_metro, custom_icons: ["#{File.cwd!()}/priv/static/icon-svg/*"] + for config_file <- Path.wildcard("config/{deps,dotcom}/*.exs") do import_config("../#{config_file}") end diff --git a/lib/dotcom/trip_plan/itinerary_group.ex b/lib/dotcom/trip_plan/itinerary_group.ex deleted file mode 100644 index 74f163a48b..0000000000 --- a/lib/dotcom/trip_plan/itinerary_group.ex +++ /dev/null @@ -1,69 +0,0 @@ -defmodule Dotcom.TripPlan.ItineraryGroups do - @moduledoc """ - Group itineraries by unique legs. - - A unique leg is defined as a leg that has a unique combination of mode, from, and to. - But, this does not include walking legs that are less than 0.2 miles. - """ - - alias Dotcom.TripPlan.{Itinerary, Leg, PersonalDetail} - - @spec from_itineraries([Itinerary.t()]) :: [ - %{departure: DateTime.t(), arrival: DateTime.t(), legs: [Leg.t()]} - ] - def from_itineraries(itineraries) do - itineraries - |> Enum.group_by(&unique_legs_to_hash/1) - |> Enum.map(&group_departures/1) - |> Enum.reject(&Enum.empty?(&1)) - end - - defp unique_legs_to_hash(legs) do - legs - |> Enum.reject(&short_walking_leg?/1) - |> Enum.map(&unique_leg_to_tuple/1) - |> :erlang.phash2() - end - - defp unique_leg_to_tuple(%Leg{mode: %PersonalDetail{}} = leg) do - {:WALK, leg.from.name, leg.to.name} - end - - defp unique_leg_to_tuple(%Leg{mode: %{route: route}} = leg) do - {Routes.Route.type_atom(route.type), leg.from.name, leg.to.name} - end - - defp group_departures({_, group}) do - group - |> Enum.uniq_by(&itinerary_to_hash/1) - |> Enum.map(fn group -> - %{ - departure: group.start, - arrival: group.stop, - legs: group.legs - } - end) - end - - defp itinerary_to_hash(itinerary) do - itinerary - |> Map.get(:legs) - |> Enum.reject(&short_walking_leg?/1) - |> Enum.map(&combined_leg_to_tuple/1) - |> :erlang.phash2() - end - - defp short_walking_leg?(%Leg{mode: %PersonalDetail{}} = leg) do - leg.distance <= 0.2 - end - - defp short_walking_leg?(_), do: false - - defp combined_leg_to_tuple(%Leg{mode: %PersonalDetail{}} = leg) do - unique_leg_to_tuple(leg) - end - - defp combined_leg_to_tuple(%Leg{mode: %{route: route}} = leg) do - {route.id, leg.from.name, leg.to.name} - end -end diff --git a/lib/dotcom/trip_plan/itinerary_groups.ex b/lib/dotcom/trip_plan/itinerary_groups.ex new file mode 100644 index 0000000000..a3b8fb58f2 --- /dev/null +++ b/lib/dotcom/trip_plan/itinerary_groups.ex @@ -0,0 +1,161 @@ +defmodule Dotcom.TripPlan.ItineraryGroups do + @moduledoc """ + Group itineraries by unique legs. + + A unique leg is defined as a leg that has a unique combination of mode, from, and to. + But, this does not include walking legs that are less than 0.2 miles. + """ + + alias Dotcom.TripPlan.{Itinerary, Leg, PersonalDetail, TransitDetail} + alias OpenTripPlannerClient.ItineraryTag + + @type summarized_leg :: %{ + routes: [Routes.Route.t()], + walk_minutes: non_neg_integer() + } + @type summary :: %{ + accessible?: boolean() | nil, + duration: non_neg_integer(), + first_start: DateTime.t(), + first_stop: DateTime.t(), + next_starts: [DateTime.t()], + summarized_legs: [summarized_leg()], + tag: String.t(), + total_cost: non_neg_integer(), + walk_distance: float() + } + + @spec from_itineraries([Itinerary.t()]) :: [ + %{itineraries: [Itinerary.t()], summary: summary()} + ] + def from_itineraries(itineraries) do + itineraries + |> Enum.group_by(&unique_legs_to_hash/1) + |> Enum.map(&group_departures/1) + |> Enum.reject(&Enum.empty?(&1)) + |> Enum.sort_by(fn + %{itineraries: [%{tag: tag} | _] = _} -> + Enum.find_index(ItineraryTag.tag_priority_order(), &(&1 == tag)) + + _ -> + -1 + end) + end + + defp unique_legs_to_hash(%Itinerary{legs: legs}) do + legs + |> Enum.reject(&short_walking_leg?/1) + |> Enum.map(&unique_leg_to_tuple/1) + |> :erlang.phash2() + end + + defp unique_leg_to_tuple(%Leg{mode: %PersonalDetail{}} = leg) do + {:WALK, leg.from.name, leg.to.name} + end + + defp unique_leg_to_tuple(%Leg{mode: %{route: route}} = leg) do + {Routes.Route.type_atom(route.type), leg.from.name, leg.to.name} + end + + defp group_departures({_hash, grouped_itineraries}) do + summarized_legs = + grouped_itineraries + |> Enum.map(& &1.legs) + |> Enum.zip_with(&Function.identity/1) + |> Enum.map(fn legs -> + legs + |> Enum.uniq_by(&combined_leg_to_tuple/1) + |> Enum.map(&to_summarized_leg/1) + |> Enum.reduce(%{walk_minutes: 0, routes: []}, &summarize_legs/2) + end) + |> remove_short_intermediate_walks() + + summary = + grouped_itineraries + |> Enum.map(fn itinerary -> + itinerary + |> Map.take([:start, :stop, :tag, :duration, :accessible?, :walk_distance]) + |> Map.put( + :total_cost, + DotcomWeb.TripPlanView.get_one_way_total_by_type(itinerary, :highest_one_way_fare) + ) + end) + |> summarize_itineraries() + |> Map.put(:summarized_legs, summarized_legs) + + %{itineraries: ItineraryTag.sort_tagged(grouped_itineraries), summary: summary} + end + + defp remove_short_intermediate_walks(summarized_legs) do + summarized_legs + |> Enum.with_index() + |> Enum.reject(fn {leg, index} -> + index > 0 && index < Kernel.length(summarized_legs) - 1 && + (leg.routes == [] && leg.walk_minutes < 5) + end) + |> Enum.map(&elem(&1, 0)) + end + + defp summarize_itineraries(itinerary_maps) do + # for most of the summary we can reflect the first itinerary + [ + %{ + tag: tag, + accessible?: accessible, + total_cost: total_cost, + duration: duration, + walk_distance: walk_distance, + stop: first_stop + } + | _ + ] = itinerary_maps + + [first_start | next_starts] = Enum.map(itinerary_maps, & &1.start) + + %{ + first_start: first_start, + first_stop: first_stop, + next_starts: next_starts, + tag: if(tag, do: Atom.to_string(tag) |> String.replace("_", " ")), + duration: duration, + accessible?: accessible, + walk_distance: walk_distance, + total_cost: total_cost + } + end + + defp summarize_legs(%{walk_minutes: new}, %{walk_minutes: old} = summary) do + %{summary | walk_minutes: new + old} + end + + defp summarize_legs( + %{route: route}, + %{routes: old_routes} = summary + ) do + # should probably sort by sort_order, but the route data returned by OTP don't have sort orders! (yet?) + routes = [route | old_routes] |> Enum.sort_by(& &1.name) + %{summary | routes: routes} + end + + defp to_summarized_leg(%Leg{mode: %PersonalDetail{}, duration: duration}) do + %{walk_minutes: duration} + end + + defp to_summarized_leg(%Leg{mode: %TransitDetail{route: route}}) do + %{route: route} + end + + defp short_walking_leg?(%Leg{mode: %PersonalDetail{}} = leg) do + leg.distance <= 0.2 + end + + defp short_walking_leg?(_), do: false + + defp combined_leg_to_tuple(%Leg{mode: %PersonalDetail{}} = leg) do + unique_leg_to_tuple(leg) + end + + defp combined_leg_to_tuple(%Leg{mode: %{route: route}} = leg) do + {route.id, leg.from.name, leg.to.name} + end +end diff --git a/lib/dotcom_web.ex b/lib/dotcom_web.ex index fb1acaf672..d9252994e8 100644 --- a/lib/dotcom_web.ex +++ b/lib/dotcom_web.ex @@ -128,7 +128,7 @@ defmodule DotcomWeb do use Gettext, backend: DotcomWeb.Gettext use MbtaMetro - import DotcomWeb.{Components, ErrorHelpers} + import DotcomWeb.{Components, Components.RouteSymbols, ErrorHelpers} import Phoenix.{HTML, LiveView.Helpers} import PhoenixHTMLHelpers.Form, except: [label: 1] import PhoenixHTMLHelpers.{Format, Link, Tag} diff --git a/lib/dotcom_web/components/components.ex b/lib/dotcom_web/components/components.ex index eb247ea8ff..1fd7d274c1 100644 --- a/lib/dotcom_web/components/components.ex +++ b/lib/dotcom_web/components/components.ex @@ -6,8 +6,6 @@ defmodule DotcomWeb.Components do """ use Phoenix.Component - alias Heroicons - attr(:id, :string, required: true, doc: "A unique identifier for this search input.") attr(:placeholder, :string, @@ -29,10 +27,11 @@ defmodule DotcomWeb.Components do ] ) - slot :inner_block, + slot(:inner_block, required: false, doc: "Additional content to render beneath the autocomplete component. With config_type='trip-planner', this can be used to render additional form elements to capture additional details about selected locations." + ) @doc """ Instantiates a search box using Algolia's Autocomplete.js library, configured diff --git a/lib/dotcom_web/components/live_components/trip_planner_form.ex b/lib/dotcom_web/components/live_components/trip_planner_form.ex index a02ef1b97b..9e7e99e7cf 100644 --- a/lib/dotcom_web/components/live_components/trip_planner_form.ex +++ b/lib/dotcom_web/components/live_components/trip_planner_form.ex @@ -4,7 +4,6 @@ defmodule DotcomWeb.Components.LiveComponents.TripPlannerForm do """ use DotcomWeb, :live_component - import DotcomWeb.ViewHelpers, only: [svg: 1] import MbtaMetro.Components.{Feedback, InputGroup} import Phoenix.HTML.Form, only: [input_value: 2] @@ -137,11 +136,9 @@ defmodule DotcomWeb.Components.LiveComponents.TripPlannerForm do -
+
<.input type="checkbox" field={f[:wheelchair]} label="Prefer accessible routes" /> - + <.icon type="icon-svg" name="icon-accessible-small" class="h-5 w-5" />
diff --git a/lib/dotcom_web/components/route_symbols.ex b/lib/dotcom_web/components/route_symbols.ex new file mode 100644 index 0000000000..e87126d9d8 --- /dev/null +++ b/lib/dotcom_web/components/route_symbols.ex @@ -0,0 +1,167 @@ +defmodule DotcomWeb.Components.RouteSymbols do + @moduledoc """ + Reusable components for displaying route information + """ + use CVA.Component + use Phoenix.Component + + import MbtaMetro.Components.Icon + + alias Routes.Route + + @logan_express_icon_names Route.logan_express_icon_names() + @massport_icon_names Route.massport_icon_names() + + defguardp is_external_route?(assigns) when not is_nil(assigns.route.external_agency_name) + + defguardp is_shuttle_route?(assigns) + when assigns.route.type == 3 and + assigns.route.description == :rail_replacement_bus and + not is_external_route?(assigns) + + variant( + :size, + [ + small: "rounded-[3px] px-[2px] py-[2px] min-w-6 text-sm", + default: "rounded-[4px] px-[4px] py-[4px] min-w-10" + ], + default: :default + ) + + attr(:class, :string, default: "") + attr(:rest, :global) + attr(:route, Routes.Route, required: true) + + @doc """ + Creates a simple symbol representing a single route. Currently shows rounded + SVG icons, small pills for bus routes, custom icons for Logan Express and + Massport routes. Supports a `size` variant defined as either `:default` or + `:small`. + + ```elixir + <.route_symbol route={%Routes.Route{id: "Red"}} size="small" /> + ``` + + Supports additional CSS class names, though they may have limited effect. Use + with caution. + + ```elixir + <.route_symbol route={%Routes.Route{type: 3}} class="mr-3" /> + ``` + + Supports custom HTML attributes. + + ```elixir + <.route_symbol route={%Routes.Route{type: 3}} data-toggle="tooltip" /> + ``` + """ + def route_symbol(%{route: %Route{description: description, type: 3}} = assigns) + when not is_external_route?(assigns) and not is_shuttle_route?(assigns) and + description != :rapid_transit do + route_class = + if(Routes.Route.silver_line?(assigns.route), + do: "bg-silver-line text-white", + else: "bg-bus text-black" + ) + + assigns = update(assigns, :class, &"#{&1} #{route_class}") + + ~H""" +
+ <%= @route.name %> +
+ """ + end + + def route_symbol(assigns), do: ~H"<.route_icon {assigns} />" + + variant( + :size, + [ + small: "w-4 h-4", + default: "w-6 h-6" + ], + default: :default + ) + + attr(:class, :string, default: "") + attr(:rest, :global) + attr(:route, Routes.Route, required: true) + + def route_icon(%{route: %Route{name: shuttle_name}} = assigns) + when is_shuttle_route?(assigns) do + route_class = + shuttle_name + |> String.replace(" Shuttle", "") + |> String.split(" ") + |> List.first() + |> then(fn replaced_route_name -> + if replaced_route_name in ["Red", "Orange", "Blue", "Green"] do + "#{String.downcase(replaced_route_name)}-line" + else + "commuter-rail" + end + end) + + assigns = update(assigns, :class, &"#{&1} text-#{route_class}") + + ~H""" + <.icon type="icon-svg" name="icon-mode-shuttle-default" class={"#{@class} #{@cva_class}"} /> + """ + end + + def route_icon(assigns) when not is_external_route?(assigns) do + assigns = + assigns + |> assign_new(:icon_name, fn %{route: route, size: size} -> + icon_name = + route + |> Routes.Route.icon_atom() + |> Atom.to_string() + |> String.replace("_", "-") + + if route.type in [0, 1] or Routes.Route.silver_line?(route) do + "icon-#{icon_name}-#{size}" + else + "icon-mode-#{icon_name}-#{size}" + end + end) + + ~H""" + <.icon type="icon-svg" name={@icon_name} class={"#{@class} #{@cva_class}"} /> + """ + end + + def route_icon( + %{ + route: %Route{ + external_agency_name: "Massport", + name: <> + } + } = assigns + ) + when route_number in @massport_icon_names do + assigns = assign(assigns, :route_number, route_number) + + ~H""" + <.icon type="icon-svg" name={"icon-massport-#{@route_number}"} class={"#{@class} #{@cva_class}"} /> + """ + end + + def route_icon(%{route: %Route{external_agency_name: "Logan Express", name: name}} = assigns) + when name in @logan_express_icon_names do + ~H""" + <.icon + type="icon-svg" + name={"icon-logan-express-#{@route.name}"} + class={"#{@class} #{@cva_class}"} + /> + """ + end + + def route_icon(assigns) do + ~H""" + <.icon type="icon-svg" name="icon-mode-shuttle-default" class={"#{@class} #{@cva_class}"} /> + """ + end +end diff --git a/lib/dotcom_web/components/trip_planner/itinerary_detail.ex b/lib/dotcom_web/components/trip_planner/itinerary_detail.ex new file mode 100644 index 0000000000..ae240f1ce7 --- /dev/null +++ b/lib/dotcom_web/components/trip_planner/itinerary_detail.ex @@ -0,0 +1,41 @@ +defmodule DotcomWeb.Components.TripPlanner.ItineraryDetail do + @moduledoc """ + A component to render an itinerary in detail.. + """ + use DotcomWeb, :component + + import DotcomWeb.Components.TripPlanner.Leg + + alias Dotcom.TripPlan.PersonalDetail + + def itinerary_detail(assigns) do + assigns = + assign( + assigns, + :all_routes, + assigns.itinerary.legs + |> Enum.reject(&match?(%PersonalDetail{}, &1.mode)) + |> Enum.map(& &1.mode.route) + ) + + ~H""" +
+ + Depart at <%= Timex.format!(@itinerary.start, "%-I:%M%p", :strftime) %> + <.route_symbol :for={route <- @all_routes} route={route} class="ml-2" /> + +
+ <.leg + start_time={leg.start} + end_time={leg.stop} + from={leg.from} + to={leg.to} + mode={leg.mode} + realtime={leg.realtime} + realtime_state={leg.realtime_state} + /> +
+
+ """ + end +end diff --git a/lib/dotcom_web/components/trip_planner/itinerary_group.ex b/lib/dotcom_web/components/trip_planner/itinerary_group.ex index dd2801a1ca..5dafdc127d 100644 --- a/lib/dotcom_web/components/trip_planner/itinerary_group.ex +++ b/lib/dotcom_web/components/trip_planner/itinerary_group.ex @@ -2,47 +2,146 @@ defmodule DotcomWeb.Components.TripPlanner.ItineraryGroup do @moduledoc """ A component to render an itinerary group. """ - use DotcomWeb, :component - import DotcomWeb.Components.TripPlanner.Leg + import DotcomWeb.Components.TripPlanner.ItineraryDetail - attr :group, :map + attr(:summary, :map, doc: "ItineraryGroups.summary()", required: true) + attr(:itineraries, :list, doc: "List of %Dotcom.TripPlan.Itinerary{}", required: true) @doc """ Renders a single itinerary group. """ def itinerary_group(assigns) do + assigns = + assign(assigns, :group_id, "group-#{:erlang.phash2(assigns.itineraries)}") + ~H""" -
- <% [first | rest] = @group %> -
Group with <%= Enum.count(@group) %> options
- <.accordion id="itinerary-group"> - <:heading> - <%= format_datetime_full(first.departure) %> — <%= format_datetime_full(first.arrival) %> - - <:content> -
- <.leg - start_time={leg.start} - end_time={leg.stop} - from={leg.from} - to={leg.to} - mode={leg.mode} - realtime={leg.realtime} - realtime_state={leg.realtime_state} - /> -
- <%= if Enum.count(rest) > 0, do: "Similar trips depart at:" %> - - <%= format_datetime_short(alternative.departure) %> - - - +
+
+ <%= @summary.tag %> +
+
+
+ <%= format_datetime_full(@summary.first_start) %> - <%= format_datetime_full( + @summary.first_stop + ) %> +
+
+ <%= @summary.duration %> min +
+
+
+ <%= for {summary_leg, index} <- Enum.with_index(@summary.summarized_legs) do %> + <.icon :if={index > 0} name="angle-right" class="font-black w-2" /> + <.leg_icon {summary_leg} /> + <% end %> +
+
+
+ <.icon type="icon-svg" name="icon-accessible-small" class="h-3 w-3 mr-0.5" /> Accessible + <.icon name="circle" class="h-0.5 w-0.5 mx-1" /> +
+
+ <.icon name="person-walking" class="h-3 w-3" /> + <%= @summary.walk_distance %> mi +
+
0} class="inline-flex items-center gap-0.5"> + <.icon name="circle" class="h-0.5 w-0.5 mx-1" /> + <.icon name="wallet" class="h-3 w-3" /> + <%= Fares.Format.price(@summary.total_cost) %> +
+
+
+
0} class="grow text-sm text-grey-dark"> + Similar trips depart at <%= Enum.map(@summary.next_starts, &format_datetime_short/1) + |> Enum.join(", ") %> +
+ +
+
""" end + attr(:class, :string, default: "") + attr(:routes, :list, required: true, doc: "List of %Routes.Route{}") + attr(:walk_minutes, :integer, required: true) + + # No routes + defp leg_icon(%{routes: [], walk_minutes: _} = assigns) do + ~H""" + + <.icon name="person-walking" class="h-4 w-4" /> + <%= @walk_minutes %> min + + """ + end + + # Group of commuter rail routes are summarized to one symbol. + defp leg_icon(%{routes: [%Routes.Route{type: 2} | _]} = assigns) do + ~H""" + <.route_symbol route={List.first(@routes)} class={@class} /> + """ + end + + # No grouping when there's only one route! + defp leg_icon(%{routes: [%Routes.Route{}]} = assigns) do + ~H""" + <.route_symbol route={List.first(@routes)} {assigns} /> + """ + end + + defp leg_icon( + %{routes: [%Routes.Route{type: type, external_agency_name: agency} | _]} = assigns + ) do + slashed? = type == 3 && is_nil(agency) + + assigns = + assigns + |> assign(:slashed?, slashed?) + |> assign( + :grouped_classes, + if(slashed?, + do: "[&:not(:first-child)]:rounded-l-none [&:not(:last-child)]:rounded-r-none", + else: "rounded-full ring-white ring-2" + ) + ) + + ~H""" +
+ <%= for {route, index} <- Enum.with_index(@routes) do %> + <.route_symbol route={route} class={"#{@grouped_classes} #{zindex(index)} #{@class}"} /> + <%= if @slashed? and index < Kernel.length(@routes) - 1 do %> +
+ <% end %> + <% end %> +
+ """ + end + + defp leg_icon(assigns) do + inspect(assigns) |> Sentry.capture_message(tags: %{feature: "Trip Planner"}) + + ~H""" + + """ + end + + defp zindex(index) do + "z-#{50 - index * 10}" + end + defp format_datetime_full(datetime) do Timex.format!(datetime, "%-I:%M %p", :strftime) end diff --git a/lib/dotcom_web/components/trip_planner/leg.ex b/lib/dotcom_web/components/trip_planner/leg.ex index 6796b354d0..88f8563171 100644 --- a/lib/dotcom_web/components/trip_planner/leg.ex +++ b/lib/dotcom_web/components/trip_planner/leg.ex @@ -5,19 +5,17 @@ defmodule DotcomWeb.Components.TripPlanner.Leg do use DotcomWeb, :component - alias DotcomWeb.PartialView.SvgIconWithCircle - alias DotcomWeb.ViewHelpers alias Dotcom.TripPlan.{PersonalDetail, TransitDetail} alias OpenTripPlannerClient.Schema.Step alias Stops.Stop - attr :from, :any - attr :to, :any - attr :start_time, :any - attr :end_time, :any - attr :mode, :any - attr :realtime, :boolean - attr :realtime_state, :string + attr(:from, :any) + attr(:to, :any) + attr(:start_time, :any) + attr(:end_time, :any) + attr(:mode, :any) + attr(:realtime, :boolean) + attr(:realtime_state, :string) def leg(assigns) do assigns = @@ -50,7 +48,7 @@ defmodule DotcomWeb.Components.TripPlanner.Leg do defp format_time(datetime), do: Timex.format!(datetime, "%-I:%M %p", :strftime) - attr :mode, :any + attr(:mode, :any) def mode(assigns) do case assigns.mode do @@ -73,12 +71,19 @@ defmodule DotcomWeb.Components.TripPlanner.Leg do def transit(assigns) do ~H"""
- <%= ViewHelpers.line_icon(@route, :default) %> (<%= @route.name %>) on trip <%= @trip_id %> + <.route_symbol route={@route} /> (<%= @route.name %>) on trip <%= @trip_id %>
  • - <%= stop.name %> <%= if Stop.accessible?(stop), - do: SvgIconWithCircle.svg_icon_with_circle(%SvgIconWithCircle{icon: :access}) %> + <%= stop.name %> +
    + <.icon + type="icon-svg" + name="icon-accessible-small" + class="h-3 w-3 mr-0.5" + aria-hidden="true" + /> Accessible +
""" diff --git a/lib/dotcom_web/live/trip_planner.ex b/lib/dotcom_web/live/trip_planner.ex index 3fbf53abcd..ad9462ff08 100644 --- a/lib/dotcom_web/live/trip_planner.ex +++ b/lib/dotcom_web/live/trip_planner.ex @@ -28,8 +28,8 @@ defmodule DotcomWeb.Live.TripPlanner do |> assign(:from, []) |> assign(:to, []) |> assign(:submitted_values, nil) - |> assign_async(:groups, fn -> - {:ok, %{groups: nil}} + |> assign_async(:results, fn -> + {:ok, %{results: nil}} end) {:ok, socket} @@ -49,21 +49,19 @@ defmodule DotcomWeb.Live.TripPlanner do

<%= submission_summary(@submitted_values) %>

<%= time_summary(@submitted_values) %>

- <.async_result :let={groups} assign={@groups}> - <:failed :let={{:error, reason}}> - <.feedback kind={:error}> - <%= Phoenix.Naming.humanize(reason) %> - + <.async_result :let={results} assign={@results}> + <:failed :let={{:error, _reason}}> + <.feedback kind={:error}>Something else went wrong. <:loading> <.spinner aria_label="Waiting for results" /> Waiting for results... - <%= if groups do %> - <%= if Enum.count(groups) == 0 do %> + <%= if results do %> + <%= if Enum.count(results) == 0 do %> <.feedback kind={:warning}>No trips found. <% else %> <.feedback kind={:success}> - Found <%= Enum.count(groups) %> <%= Inflex.inflect("way", Enum.count(groups)) %> to go. + Found <%= Enum.count(results) %> <%= Inflex.inflect("way", Enum.count(results)) %> to go. <% end %> <% end %> @@ -73,9 +71,9 @@ defmodule DotcomWeb.Live.TripPlanner do
<%= inspect(@error) %>
- <.async_result :let={groups} assign={@groups}> -
- <.itinerary_group :for={group <- groups} group={group} /> + <.async_result :let={results} assign={@results}> +
+ <.itinerary_group :for={result <- results} {result} />
<.live_component @@ -110,12 +108,11 @@ defmodule DotcomWeb.Live.TripPlanner do socket = socket |> assign(:submitted_values, data) - |> assign(:groups, nil) - |> assign_async(:groups, fn -> + |> assign(:results, nil) + |> assign_async(:results, fn -> case Dotcom.TripPlan.OpenTripPlanner.plan(data) do {:ok, itineraries} -> - Process.sleep(1200) - {:ok, %{groups: ItineraryGroups.from_itineraries(itineraries)}} + {:ok, %{results: ItineraryGroups.from_itineraries(itineraries)}} error -> error diff --git a/lib/dotcom_web/templates/alert/show.html.eex b/lib/dotcom_web/templates/alert/show.html.heex similarity index 73% rename from lib/dotcom_web/templates/alert/show.html.eex rename to lib/dotcom_web/templates/alert/show.html.heex index e56816fab9..cb34431acb 100644 --- a/lib/dotcom_web/templates/alert/show.html.eex +++ b/lib/dotcom_web/templates/alert/show.html.heex @@ -7,12 +7,12 @@
- <%= DotcomWeb.PartialView.alert_time_filters(@alerts_timeframe, [ + <%= DotcomWeb.PartialView.alert_time_filters(@alerts_timeframe, method: :alert_path, item: @id - ]) %> + ) %>
- <%= render "_t-alerts.html" %> + <%= render("_t-alerts.html") %>
@@ -24,15 +24,17 @@

Systemwide

- <%= - DotcomWeb.PartialView.SvgIconWithCircle.svg_icon_with_circle( - %SvgIconWithCircle{icon: :t_logo, aria_hidden?: true} - ) - %> + <%= DotcomWeb.PartialView.SvgIconWithCircle.svg_icon_with_circle(%SvgIconWithCircle{ + icon: :t_logo, + aria_hidden?: true + }) %>
- <%= render "_item.html", alert: %{Alerts.Repo.by_id(@alert_banner.id) | priority: :system}, date_time: @date_time %> + <%= render("_item.html", + alert: %{Alerts.Repo.by_id(@alert_banner.id) | priority: :system}, + date_time: @date_time + ) %>
<% end %> @@ -44,12 +46,16 @@ <% end %> <%= if show_mode_icon?(route_or_stop) do %> <%= link to: group_header_path(route_or_stop), class: "m-alerts-header__icon" do %> - <%= route_icon(route_or_stop) %> + <.route_icon route={route_or_stop} /> <% end %> <% end %>
- <%= DotcomWeb.AlertView.group alerts: alerts, route: route_or_stop, date_time: @date_time %> + <%= DotcomWeb.AlertView.group( + alerts: alerts, + route: route_or_stop, + date_time: @date_time + ) %>
<% end %> <%= if Enum.empty?(@alert_groups) && !show_systemwide_alert? do %> @@ -58,7 +64,7 @@
<% end %>
- <%= render "_t-alerts.html" %> + <%= render("_t-alerts.html") %>
diff --git a/lib/dotcom_web/views/alert_view.ex b/lib/dotcom_web/views/alert_view.ex index d2603e167c..b47d1e14e7 100644 --- a/lib/dotcom_web/views/alert_view.ex +++ b/lib/dotcom_web/views/alert_view.ex @@ -5,7 +5,6 @@ defmodule DotcomWeb.AlertView do import DotcomWeb.ViewHelpers import PhoenixHTMLHelpers.Tag, only: [content_tag: 3] - import DotcomWeb.PartialView.SvgIconWithCircle, only: [svg_icon_with_circle: 1] alias Alerts.{Alert, InformedEntity, InformedEntitySet, URLParsingHelpers} alias DotcomWeb.PartialView.SvgIconWithCircle @@ -236,15 +235,6 @@ defmodule DotcomWeb.AlertView do defp show_mode_icon?(%Route{}), do: true - @spec route_icon(Route.t()) :: Phoenix.HTML.Safe.t() - def route_icon(%Route{type: 3, description: :rapid_transit}) do - svg_icon_with_circle(%SvgIconWithCircle{icon: :silver_line, aria_hidden?: true}) - end - - def route_icon(%Route{} = route) do - svg_icon_with_circle(%SvgIconWithCircle{icon: Route.icon_atom(route), aria_hidden?: true}) - end - @spec mode_buttons(atom) :: [Phoenix.HTML.Safe.t()] def mode_buttons(selected) do for mode <- [:subway, :bus, :commuter_rail, :ferry, :access] do diff --git a/lib/dotcom_web/views/helpers.ex b/lib/dotcom_web/views/helpers.ex index d91069e76b..09e409d0c6 100644 --- a/lib/dotcom_web/views/helpers.ex +++ b/lib/dotcom_web/views/helpers.ex @@ -16,11 +16,6 @@ defmodule DotcomWeb.ViewHelpers do alias Plug.Conn alias Routes.Route - # Icons we know we have SVGs for, modify if new icons are added/removed - # These names are equivalent to the route names from the Massport GTFS - @massport_icon_names ["11", "22", "33", "44", "55", "66", "77", "88", "99"] - @logan_express_icon_names ["BB", "BT", "DV", "FH", "WO"] - @stops_repo Application.compile_env!(:dotcom, :repo_modules)[:stops] @subway_lines [ @@ -124,7 +119,7 @@ defmodule DotcomWeb.ViewHelpers do when is_binary(name) do route_number = String.slice(name, 0..1) - if route_number in @massport_icon_names do + if route_number in Route.massport_icon_names() do svg("icon-massport-#{route_number}.svg") else report_missing_icon("route Massport #{route_number}") @@ -135,7 +130,7 @@ defmodule DotcomWeb.ViewHelpers do # Logan Express shuttle routes def line_icon(%Route{external_agency_name: "Logan Express", name: name}, _) when is_binary(name) do - if name in @logan_express_icon_names do + if name in Route.logan_express_icon_names() do svg("icon-logan-express-#{name}.svg") else report_missing_icon("route Logan Express #{name}") diff --git a/lib/routes/route.ex b/lib/routes/route.ex index 8f275dcba6..9b337e8b67 100644 --- a/lib/routes/route.ex +++ b/lib/routes/route.ex @@ -85,9 +85,16 @@ defmodule Routes.Route do @type subway_lines_type :: :orange_line | :red_line | :green_line | :blue_line | :mattapan_line @type branch_name :: String.t() | nil + # Icons we know we have SVGs for, modify if new icons are added/removed + # These names are equivalent to the route names from the Massport GTFS + @logan_express_icon_names ["BB", "BT", "DV", "FH", "WO"] + @massport_icon_names ["11", "22", "33", "44", "55", "66", "77", "88", "99"] @silver_line ~w(741 742 743 746 749 751) @silver_line_set MapSet.new(@silver_line) + def logan_express_icon_names, do: @logan_express_icon_names + def massport_icon_names, do: @massport_icon_names + @spec type_atom(t | type_int | String.t()) :: route_type def type_atom(%__MODULE__{external_agency_name: "Massport"}), do: :massport_shuttle def type_atom(%__MODULE__{external_agency_name: "Logan Express"}), do: :logan_express diff --git a/mix.exs b/mix.exs index 3ec4e57887..61c74fc75e 100644 --- a/mix.exs +++ b/mix.exs @@ -77,6 +77,7 @@ defmodule DotCom.Mixfile do {:crc, "0.10.5"}, {:credo, "1.7.8", only: [:dev, :test]}, {:csv, "3.2.1"}, + {:cva, "0.2.1"}, {:decorator, "1.4.0"}, {:dialyxir, "1.4.4", [only: [:dev, :test], runtime: false]}, {:diskusage_logger, "0.2.0"}, @@ -111,7 +112,7 @@ defmodule DotCom.Mixfile do {:nebulex_redis_adapter, "2.4.1"}, { :open_trip_planner_client, - [github: "thecristen/open_trip_planner_client", tag: "v0.10.5"] + [github: "thecristen/open_trip_planner_client", tag: "v0.10.6"] }, {:parallel_stream, "1.1.0"}, # latest version 1.7.14 diff --git a/mix.lock b/mix.lock index e542a1199b..2658d87554 100644 --- a/mix.lock +++ b/mix.lock @@ -14,6 +14,7 @@ "crc": {:hex, :crc, "0.10.5", "ee12a7c056ac498ef2ea985ecdc9fa53c1bfb4e53a484d9f17ff94803707dfd8", [:mix, :rebar3], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3e673b6495a9525c5c641585af1accba59a1eb33de697bedf341e247012c2c7f"}, "credo": {:hex, :credo, "1.7.8", "9722ba1681e973025908d542ec3d95db5f9c549251ba5b028e251ad8c24ab8c5", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cb9e87cc64f152f3ed1c6e325e7b894dea8f5ef2e41123bd864e3cd5ceb44968"}, "csv": {:hex, :csv, "3.2.1", "6d401f1ed33acb2627682a9ab6021e96d33ca6c1c6bccc243d8f7e2197d032f5", [:mix], [], "hexpm", "8f55a0524923ae49e97ff2642122a2ce7c61e159e7fe1184670b2ce847aee6c8"}, + "cva": {:hex, :cva, "0.2.1", "fe8acf9c6031714c9b671d80fc7728bd1cc9f5df7708669cdb9682856d352f71", [:mix], [{:phoenix_live_view, "~> 0.18", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "f913a0367e1aed19df18fbfd0672e0abf48b1ac387432b9b4c32883cd4b0341d"}, "decimal": {:hex, :decimal, "2.2.0", "df3d06bb9517e302b1bd265c1e7f16cda51547ad9d99892049340841f3e15836", [:mix], [], "hexpm", "af8daf87384b51b7e611fb1a1f2c4d4876b65ef968fa8bd3adf44cff401c7f21"}, "decorator": {:hex, :decorator, "1.4.0", "a57ac32c823ea7e4e67f5af56412d12b33274661bb7640ec7fc882f8d23ac419", [:mix], [], "hexpm", "0a07cedd9083da875c7418dea95b78361197cf2bf3211d743f6f7ce39656597f"}, "dialyxir": {:hex, :dialyxir, "1.4.4", "fb3ce8741edeaea59c9ae84d5cec75da00fa89fe401c72d6e047d11a61f65f70", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "cd6111e8017ccd563e65621a4d9a4a1c5cd333df30cebc7face8029cacb4eff6"}, @@ -71,7 +72,7 @@ "nimble_ownership": {:hex, :nimble_ownership, "1.0.0", "3f87744d42c21b2042a0aa1d48c83c77e6dd9dd357e425a038dd4b49ba8b79a1", [:mix], [], "hexpm", "7c16cc74f4e952464220a73055b557a273e8b1b7ace8489ec9d86e9ad56cb2cc"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, - "open_trip_planner_client": {:git, "https://github.com/thecristen/open_trip_planner_client.git", "81c5778851409fe824ea63b5d50783318217e1af", [tag: "v0.10.5"]}, + "open_trip_planner_client": {:git, "https://github.com/thecristen/open_trip_planner_client.git", "ca4112f6b2133404b3f99afbf9eaeea6d42c0c21", [tag: "v0.10.6"]}, "parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, diff --git a/priv/static/icon-svg/icon-mode-shuttle-default.svg b/priv/static/icon-svg/icon-mode-shuttle-default.svg new file mode 100644 index 0000000000..bd024d7872 --- /dev/null +++ b/priv/static/icon-svg/icon-mode-shuttle-default.svg @@ -0,0 +1 @@ +Shuttle Bus diff --git a/test/dotcom_web/components/route_symbols_test.exs b/test/dotcom_web/components/route_symbols_test.exs new file mode 100644 index 0000000000..0e1743bdd7 --- /dev/null +++ b/test/dotcom_web/components/route_symbols_test.exs @@ -0,0 +1,113 @@ +defmodule DotcomWeb.Components.RouteSymbolsTest do + @moduledoc false + use ExUnit.Case + + import Phoenix.LiveViewTest + import DotcomWeb.Components.RouteSymbols + import Test.Support.Factories.Routes.Route + + describe "route_symbol/1" do + test "supports size variant" do + route = build(:route) + + assert render_component(&route_symbol/1, %{route: route, size: :default}) != + render_component(&route_symbol/1, %{route: route, size: :small}) + end + + test "handles Logan Express, falling back to generic shuttle bus" do + assert render_component(&route_symbol/1, %{ + route: build(:logan_express_route) + }) + |> matches_title?("Logan Express") + + assert render_component(&route_symbol/1, %{ + route: build(:logan_express_route, name: "unknown") + }) + |> matches_title?("Shuttle Bus") + end + + test "handles Massport, falling back to generic shuttle bus" do + assert render_component(&route_symbol/1, %{ + route: build(:massport_route) + }) + |> matches_title?("Massport") + + assert render_component(&route_symbol/1, %{ + route: build(:massport_route, name: "unknown") + }) + |> matches_title?("Shuttle Bus") + end + + test "handles buses" do + route = build(:bus_route) + + assert render_component(&route_symbol/1, %{ + route: route + }) =~ route.name + end + + test "handles Silver Live rapid transit" do + route = + build(:bus_route, + description: :rapid_transit, + id: Faker.Util.pick(Routes.Route.silver_line()) + ) + + assert render_component(&route_symbol/1, %{ + route: route + }) + |> matches_title?("Silver Line") + end + + test "handles Silver Live buses" do + route = + build(:bus_route, + id: Faker.Util.pick(Routes.Route.silver_line()) + ) + + assert render_component(&route_symbol/1, %{ + route: route + }) =~ route.name + end + + test "handles rail replacement buses" do + replaced_route = Faker.Util.pick(~w(Red Orange Blue Green Worcester)) + route_name = replaced_route <> " Line Shuttle" + + route = + build(:bus_route, + description: :rail_replacement_bus, + name: route_name + ) + + icon = + render_component(&route_symbol/1, %{ + route: route + }) + + assert icon |> matches_title?("Shuttle Bus") + classnames = icon |> Floki.attribute("class") |> List.first() + + assert classnames =~ "text-#{String.downcase(replaced_route)}-line" || + classnames =~ "text-commuter-rail" + end + end + + describe "route_icon/1" do + test "supports size variant" do + route = build(:route) + + assert render_component(&route_icon/1, %{route: route, size: :default}) != + render_component(&route_icon/1, %{route: route, size: :small}) + end + end + + defp matches_title?(html, text) do + html + |> Floki.find("title") + |> Floki.text() + |> String.trim() + |> Regex.compile!("i") + |> Regex.match?(text) + end +end diff --git a/test/dotcom_web/controllers/alert_controller_test.exs b/test/dotcom_web/controllers/alert_controller_test.exs index 11f980bcd1..67a6205b54 100644 --- a/test/dotcom_web/controllers/alert_controller_test.exs +++ b/test/dotcom_web/controllers/alert_controller_test.exs @@ -2,6 +2,9 @@ defmodule DotcomWeb.AlertControllerTest do use DotcomWeb.ConnCase, async: true use Phoenix.Controller + + import Phoenix.LiveViewTest + alias Alerts.Alert alias DotcomWeb.PartialView.SvgIconWithCircle alias Stops.Stop @@ -92,9 +95,10 @@ defmodule DotcomWeb.AlertControllerTest do response = render_alerts_page(conn, :subway, alerts) expected = - %SvgIconWithCircle{icon: :red_line, aria_hidden?: true} - |> SvgIconWithCircle.svg_icon_with_circle() - |> Phoenix.HTML.safe_to_string() + render_component( + &DotcomWeb.Components.RouteSymbols.route_symbol/1, + %{route: get_route(:subway), size: :default} + ) assert response =~ expected end diff --git a/test/dotcom_web/views/alert_view_test.exs b/test/dotcom_web/views/alert_view_test.exs index 1f6214b949..6066e82324 100644 --- a/test/dotcom_web/views/alert_view_test.exs +++ b/test/dotcom_web/views/alert_view_test.exs @@ -25,40 +25,6 @@ defmodule DotcomWeb.AlertViewTest do end end - describe "route_icon/1" do - test "silver line icon" do - icon = - %Routes.Route{ - description: :rapid_transit, - direction_names: %{0 => "Outbound", 1 => "Inbound"}, - id: "742", - long_name: "Design Center - South Station", - name: "SL2", - type: 3 - } - |> route_icon() - |> safe_to_string() - - icon =~ "Silver Line" - end - - test "red line icon" do - icon = - %Routes.Route{ - description: :rapid_transit, - direction_names: %{0 => "Southbound", 1 => "Northbound"}, - id: "Red", - long_name: "Red Line", - name: "Red Line", - type: 1 - } - |> route_icon() - |> safe_to_string() - - icon =~ "Red Line" - end - end - describe "alert_updated/1" do test "returns the relative offset based on our timezone" do now = ~N[2016-10-05T00:02:03] diff --git a/test/support/factories/routes/route.ex b/test/support/factories/routes/route.ex index cd7d0f82f3..61a333c97a 100644 --- a/test/support/factories/routes/route.ex +++ b/test/support/factories/routes/route.ex @@ -8,6 +8,48 @@ defmodule Test.Support.Factories.Routes.Route do alias Routes.Route alias Test.Support.FactoryHelpers + @logan_express_icon_names Route.logan_express_icon_names() + @massport_icon_names Route.massport_icon_names() + + def bus_route_factory(attrs) do + %{ + description: + Faker.Util.pick([ + :local_bus, + :key_bus_route, + :supplemental_bus, + :commuter_bus, + :community_bus + ]), + external_agency_name: nil, + type: 3 + } + |> Map.merge(attrs) + |> route_factory() + end + + def logan_express_route_factory(attrs) do + %{ + description: nil, + external_agency_name: "Logan Express", + name: Faker.Util.pick(@logan_express_icon_names), + type: 3 + } + |> Map.merge(attrs) + |> route_factory() + end + + def massport_route_factory(attrs) do + %{ + description: nil, + external_agency_name: "Massport", + name: Faker.Util.pick(@massport_icon_names), + type: 3 + } + |> Map.merge(attrs) + |> route_factory() + end + def route_factory(attrs) do type = attrs[:type] || Faker.Util.pick([0, 1, 2, 3, 4]) fare_class = fare_class(attrs, type)