Files
archived-teslamate/test/teslamate_web/controllers/car_controller_test.exs
T
marvelmandJakobLichterfeld da819054f6 feat: Upgrade web development framework to Phoenix 1.7 (#3615)
* Upgrade to phoenix 1.7.0
The minimal changes necessary to compile the application

* Import Phoenix.Component instead of LiveView

* Fix compilation warnings

* mix format

* Update tests to trim newline characters

* mix gettext.extract --merge

* style: correct linting

* rebuild gettext

* fix: correct spacing before unit for energy added

* fix: use unicode character for whitespace

* fix: spacing with witespace

* fix: use numeric value for charge_energy_added in charging test

* fix: avoid warning about unsued function, which is actually used in plug

* fix: use whitespace in test before charge_energy_added

---------

Co-authored-by: JakobLichterfeld <jakob-lichterfeld@gmx.de>
2024-02-07 19:23:53 +01:00

634 lines
19 KiB
Elixir
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
defmodule TeslaMateWeb.CarControllerTest do
use TeslaMateWeb.ConnCase
use TeslaMate.VehicleCase
alias TeslaMate.Settings.CarSettings
alias TeslaMate.{Log, Settings, Repo}
alias TeslaMate.Log.Car
defp table_row(html, key, value, opts \\ []) do
assert {"tr", _, [{"td", _, [td_key]}, {"td", [], [td_value]}]} =
html
|> Floki.parse_document!()
|> Floki.find("tr")
|> Enum.find(fn ele ->
Floki.text(ele) =~ key
end)
assert Floki.text(td_key) |> String.trim() == key
case Keyword.get(opts, :tooltip) do
nil ->
assert Floki.text(value) == Floki.text(td_value) |> String.trim()
str ->
assert {"span", [_, {"data-tooltip", ^str}], [ele]} = td_value
assert Floki.text(ele) |> String.trim() == value
end
end
defp icon(html, tooltip, icon) do
icon_class = "mdi mdi-#{icon}"
assert {"span", _, [{"span", [{"class", ^icon_class}], _}]} =
html
|> Floki.parse_document!()
|> Floki.find(".icons .icon")
|> Enum.find(&match?({"span", [_, {"data-tooltip", ^tooltip}], _}, &1))
end
defp car_fixture(settings) do
{:ok, car} =
Log.create_car(%{
efficiency: 0.153,
eid: 4242,
vid: 404,
vin: "xxxxx",
model: "S",
name: "foo",
trim_badging: "P100D"
})
{:ok, _settings} =
car.settings
|> Repo.preload(:car)
|> Settings.update_car_settings(settings)
car
end
describe "index" do
test "redirects if not signed in", %{conn: conn} do
assert conn = get(conn, Routes.car_path(conn, :index))
assert redirected_to(conn, 302) == Routes.live_path(conn, TeslaMateWeb.SignInLive.Index)
end
@tag :signed_in
test "lists all active vehicles", %{conn: conn} do
{:ok, _pid} =
start_supervised(
{ApiMock, name: :api_vehicle, events: [{:ok, online_event()}], pid: self()}
)
{:ok, _pid} =
start_supervised(
{TeslaMate.Vehicles,
vehicle: VehicleMock,
vehicles: [
%TeslaApi.Vehicle{display_name: "f00o", id: 4241, vehicle_id: 11111, vin: "1221"},
%TeslaApi.Vehicle{display_name: "foo0", id: 1242, vehicle_id: 22222, vin: "2112"}
]}
)
conn = get(conn, Routes.car_path(conn, :index))
html = response(conn, 200)
assert [
{"div", [{"class", "car card"}], _},
{"div", [{"class", "car card"}], _}
] =
html
|> Floki.parse_document!()
|> Floki.find(".car")
end
@tag :signed_in
test "renders last known vehicle stats", %{conn: conn} do
events = [
{:ok, %TeslaApi.Vehicle{state: "asleep", display_name: "FooCar"}}
]
{:ok, car} =
%Car{settings: %CarSettings{}}
|> Car.changeset(%{vid: 404, eid: 404, vin: "xxxxx"})
|> Log.create_or_update_car()
{:ok, _position} =
Log.insert_position(car, %{
date: DateTime.utc_now(),
longitude: 0,
latitude: 0,
rated_battery_range_km: 380.25,
est_battery_range_km: 401.52,
ideal_battery_range_km: 175.1,
battery_level: 80,
outside_temp: 20.1,
inside_temp: 21.0
})
:ok = start_vehicles(events)
conn = get(conn, Routes.car_path(conn, :index))
assert html = response(conn, 200)
assert html =~ ~r/<p class="title is-5">FooCar<\/p>/
assert table_row(html, "Status", "asleep")
assert table_row(html, "Range (rated)", "380.26 km")
assert table_row(html, "Range (est.)", "401.52 km")
assert table_row(html, "State of Charge", "80%", tooltip: "≈ 475 km at 100%")
assert table_row(html, "Outside Temperature", "20.1 °C")
assert table_row(html, "Inside Temperature", "21.0 °C")
end
@tag :signed_in
test "displays the friendly name", %{conn: conn} do
now = (DateTime.utc_now() |> DateTime.to_unix()) * 1000
events = [
{:ok,
online_event(
display_name: "FooCar",
drive_state: %{timestamp: now, latitude: 0.0, longitude: 0.0},
vehicle_config: %{car_type: "model3", trim_badging: "p74d"}
)}
]
:ok = start_vehicles(events)
Process.sleep(250)
conn = get(conn, Routes.car_path(conn, :index))
assert html = response(conn, 200)
assert "Model 3 LR AWD Performance" ==
html
|> Floki.parse_document!()
|> Floki.find(".car .subtitle")
|> Floki.text()
# Trim all whitespace
|> String.trim()
|> String.replace(~r/(\n)+/, " ")
|> String.replace(~r/(\s)+/, " ")
|> String.replace(~r/(\t)+/, " ")
end
@tag :signed_in
test "renders current vehicle stats [:online]", %{conn: conn} do
now = (DateTime.utc_now() |> DateTime.to_unix()) * 1000
events = [
{:ok,
online_event(
display_name: "FooCar",
drive_state: %{timestamp: now, latitude: 0.0, longitude: 0.0},
charge_state: %{
battery_range: 200,
est_battery_range: 180,
ideal_battery_range: 175,
usable_battery_level: 67,
battery_level: 69
},
climate_state: %{is_preconditioning: true, outside_temp: 24, inside_temp: 23.2},
vehicle_state: %{
timestamp: 0,
car_version: "2019.40.50.7 ad132c7b057e",
software_update: %{status: "available", version: "2020.4.1 4a4ad401858f"},
locked: true,
sentry_mode: true,
fd_window: 1,
fp_window: 0,
rd_window: 0,
rp_window: 0,
df: 0,
dr: 0,
pf: 1,
pr: 0,
is_user_present: true,
odometer: 26097.59
},
vehicle_config: %{car_type: "models2", trim_badging: "p90d"}
)}
]
:ok = start_vehicles(events)
Process.sleep(250)
conn = get(conn, Routes.car_path(conn, :index))
assert html = response(conn, 200)
assert html =~ ~r/<p class="title is-5">FooCar<\/p>/
assert "Model S P90D" ==
html
|> Floki.parse_document!()
|> Floki.find(".car .subtitle")
|> Floki.text()
|> String.trim()
|> String.replace(~r/(\n)+/, " ")
|> String.replace(~r/(\s)+/, " ")
assert table_row(html, "Status", "online")
assert table_row(html, "Range (rated)", "321.87 km")
assert table_row(html, "Range (est.)", "289.68 km")
assert table_row(html, "State of Charge", "67% (69%)", tooltip: "≈ 480 km at 100%")
assert icon(html, "Locked", "lock")
assert icon(html, "Driver present", "account")
assert icon(html, "Preconditioning", "air-conditioner")
assert icon(html, "Sentry Mode", "shield-check")
assert icon(html, "Windows open", "window-open")
assert icon(html, "Doors open", "car-door")
assert icon(html, "Software Update available (2020.4.1)", "gift-outline")
assert table_row(html, "Outside Temperature", "24 °C")
assert table_row(html, "Inside Temperature", "23.2 °C")
assert table_row(html, "Mileage", "42000 km")
assert table_row(
html,
"Version",
{"a",
[
{"href",
"https://www.notateslaapp.com/software-updates/version/2019.40.50.7/release-notes"},
{"rel", "noopener noreferrer"},
{"target", "_blank"}
], ["2019.40.50.7"]}
)
end
@tag :signed_in
test "renders current vehicle stats [:charging]", %{conn: conn} do
car = car_fixture(%{})
events = [
{:ok,
online_event(
display_name: "FooCar",
drive_state: %{timestamp: 0, latitude: 0.0, longitude: 0.0},
charge_state: %{
timestamp: 0,
charger_power: 11,
charger_phases: 3,
charger_voltage: 229,
charger_actual_current: 16,
battery_range: 200,
est_battery_range: 180,
ideal_battery_range: 175,
charging_state: "Charging",
charge_energy_added: 4.32,
charge_port_latch: "Engaged",
charge_port_door_open: true,
charge_port_cold_weather_mode: false,
scheduled_charging_start_time: 1_565_620_707,
charge_limit_soc: 85,
time_to_full_charge: 1.83
}
)}
]
:ok = start_vehicles(events)
conn = get(conn, Routes.car_path(conn, :index))
assert html = response(conn, 200)
assert html =~ ~r/<p class="title is-5">FooCar<\/p>/
assert table_row(html, "Status", "charging")
assert table_row(html, "Remaining Time", "1 h, 49 min")
assert icon(html, "Plugged In", "power-plug")
assert table_row(html, "Range (rated)", "321.87 km")
assert table_row(html, "Range (est.)", "289.68 km")
assert table_row(html, "Charged", "4.32 kWh")
assert table_row(html, "Charger Power", "11 kW")
assert table_row(
html,
"Scheduled Charging",
{"span",
[
{"data-date", "2019-08-12T14:38:27Z"},
{"id", "scheduled_start_time_#{car.id}"},
{"phx-hook", "LocalTime"}
], []}
)
assert table_row(html, "Charge Limit", "85%")
end
@tag :signed_in
test "does not render remaining seconds", %{conn: conn} do
events = [
{:ok,
online_event(
drive_state: %{timestamp: 0, latitude: 0.0, longitude: 0.0},
charge_state: %{
timestamp: 0,
charging_state: "Charging",
charge_energy_added: "4.32",
ideal_battery_range: 200,
time_to_full_charge: 0.33
}
)}
]
:ok = start_vehicles(events)
conn = get(conn, Routes.car_path(conn, :index))
assert html = response(conn, 200)
assert table_row(html, "Remaining Time", "19 min")
end
@tag :signed_in
test "renders current vehicle stats [:driving]", %{conn: conn} do
events = [
{:ok,
online_event(
display_name: "FooCar",
drive_state: %{
timestamp: 0,
latitude: 0.0,
longitude: 0.0,
shift_state: "D",
speed: 30
}
)}
]
:ok = start_vehicles(events)
conn = get(conn, Routes.car_path(conn, :index))
assert html = response(conn, 200)
assert html =~ ~r/<p class="title is-5">FooCar<\/p>/
assert table_row(html, "Status", "driving")
assert table_row(html, "Speed", "48 km/h")
end
@tag :signed_in
test "renders current vehicle stats [:updating]", %{conn: conn} do
alias TeslaApi.Vehicle.State.VehicleState.SoftwareUpdate
events = [
{:ok,
online_event(
display_name: "FooCar",
vehicle_state: %{
timestamp: 0,
car_version: "2019.8.4 530d1d3",
software_update: %SoftwareUpdate{expected_duration_sec: 2700, status: "installing"}
}
)}
]
:ok = start_vehicles(events)
conn = get(conn, Routes.car_path(conn, :index))
assert html = response(conn, 200)
assert html =~ ~r/<p class="title is-5">FooCar<\/p>/
assert table_row(html, "Status", "updating")
end
@tag :signed_in
test "renders current vehicle stats [:asleep]", %{conn: conn} do
events = [
{:ok, %TeslaApi.Vehicle{display_name: "FooCar", state: "asleep"}}
]
:ok = start_vehicles(events)
conn = get(conn, Routes.car_path(conn, :index))
assert html = response(conn, 200)
assert html =~ ~r/<p class="title is-5">FooCar<\/p>/
assert table_row(html, "Status", "asleep")
end
@tag :signed_in
test "renders current vehicle stats [:offline]", %{conn: conn} do
events = [
{:ok, %TeslaApi.Vehicle{display_name: "FooCar", state: "offline"}}
]
:ok = start_vehicles(events)
conn = get(conn, Routes.car_path(conn, :index))
assert html = response(conn, 200)
assert html =~ ~r/<p class="title is-5">FooCar<\/p>/
assert table_row(html, "Status", "offline")
end
@tag :signed_in
test "renders current vehicle stats [:falling asleep]", %{conn: conn} do
_car = car_fixture(%{suspend_min: 60, suspend_after_idle_min: 1, use_streaming_api: false})
events = [
{:ok,
online_event(
display_name: "FooCar",
drive_state: %{timestamp: 0, latitude: 0.0, longitude: 0.0},
climate_state: %{is_preconditioning: false}
)}
]
:ok = start_vehicles(events)
Process.sleep(100)
conn = get(conn, Routes.car_path(conn, :index))
assert html = response(conn, 200)
assert html =~ ~r/<p class="title is-5">FooCar<\/p>/
assert table_row(html, "Status", "falling asleep")
end
@tag :capture_log
@tag :signed_in
test "renders current vehicle stats [:unavailable]", %{conn: conn} do
events = [
{:error, :unknown}
]
:ok = start_vehicles(events)
conn = get(conn, Routes.car_path(conn, :index))
assert html = response(conn, 200)
assert html =~ ~r/<p class="title is-5"><\/p>/
assert table_row(html, "Status", "unavailable")
end
@tag :signed_in
test "displays the rated range if preferred", %{conn: conn} do
{:ok, _} =
Settings.get_global_settings!()
|> Settings.update_global_settings(%{preferred_range: :rated})
events = [
{:ok,
online_event(
display_name: "FooCar",
drive_state: %{timestamp: 0, latitude: 0.0, longitude: 0.0},
charge_state: %{
ideal_battery_range: 200,
est_battery_range: 180,
battery_range: 175,
battery_level: 69
},
climate_state: %{is_preconditioning: false, outside_temp: 24, inside_temp: 23.2},
vehicle_state: %{timestamp: 0, locked: true, sentry_mode: true, car_version: ""},
vehicle_config: %{car_type: "models2", trim_badging: "p90d"}
)}
]
:ok = start_vehicles(events)
Process.sleep(250)
conn = get(conn, Routes.car_path(conn, :index))
assert html = response(conn, 200)
assert html =~ ~r/<p class="title is-5">FooCar<\/p>/
assert table_row(html, "Range (rated)", "281.64 km")
assert table_row(html, "Range (est.)", "289.68 km")
end
@tag :signed_in
@tag :capture_log
test "displays imperial units", %{conn: conn} do
{:ok, _} =
Settings.get_global_settings!()
|> Settings.update_global_settings(%{unit_of_length: :mi, unit_of_temperature: :F})
events = [
{:ok,
online_event(
display_name: "FooCar",
drive_state: %{
timestamp: 0,
latitude: 0.0,
longitude: 0.0,
shift_state: "D",
speed: 30
},
charge_state: %{
battery_range: 200,
est_battery_range: 180,
ideal_battery_range: 175,
usable_battery_level: 67,
battery_level: 69
},
climate_state: %{
is_preconditioning: false,
outside_temp: 24,
inside_temp: 23.2
},
vehicle_state: %{
odometer: 42000
}
)}
]
:ok = start_vehicles(events)
conn = get(conn, Routes.car_path(conn, :index))
assert html = response(conn, 200)
assert html =~ ~r/<p class="title is-5">FooCar<\/p>/
assert table_row(html, "Status", "driving")
assert table_row(html, "Range (rated)", "200.0 mi")
assert table_row(html, "Range (est.)", "180.0 mi")
assert table_row(html, "State of Charge", "67% (69%)", tooltip: "≈ 299 mi at 100%")
assert table_row(html, "Speed", "30 mph")
assert table_row(html, "Outside Temperature", "75.2 °F")
assert table_row(html, "Inside Temperature", "73.8 °F")
assert table_row(html, "Mileage", "42000 mi")
end
end
describe "suspend" do
setup %{conn: conn} do
{:ok, conn: put_req_header(conn, "accept", "application/json")}
end
test "suspends logging", %{conn: conn} do
_car = car_fixture(%{suspend_min: 60, suspend_after_idle_min: 60, use_streaming_api: false})
events = [
{:ok,
online_event(
display_name: "FooCar",
drive_state: %{timestamp: 0, latitude: 0.0, longitude: 0.0},
climate_state: %{is_preconditioning: false}
)}
]
:ok = start_vehicles(events)
%Car{id: id} = Log.get_car_by(vin: "xxxxx")
conn = put(conn, Routes.car_path(conn, :suspend_logging, id))
assert "" == response(conn, 204)
end
test "returns error if suspending is not possible", %{conn: conn} do
_car = car_fixture(%{suspend_min: 60, suspend_after_idle_min: 60, use_streaming_api: false})
events = [
{:ok,
online_event(
display_name: "FooCar",
drive_state: %{timestamp: 0, latitude: 0.0, longitude: 0.0},
climate_state: %{is_preconditioning: true}
)}
]
:ok = start_vehicles(events)
%Car{id: id} = Log.get_car_by(vin: "xxxxx")
conn = put(conn, Routes.car_path(conn, :suspend_logging, id))
assert "preconditioning" == json_response(conn, 412)["error"]
end
end
describe "resume" do
test "resumes logging", %{conn: conn} do
alias TeslaMate.Vehicles.Vehicle.Summary
_car = car_fixture(%{suspend_min: 60, suspend_after_idle_min: 1, use_streaming_api: false})
events = [
{:ok,
online_event(
display_name: "FooCar",
drive_state: %{timestamp: 0, latitude: 0.0, longitude: 0.0},
climate_state: %{is_preconditioning: false}
)}
]
:ok = start_vehicles(events)
Process.sleep(100)
%Car{id: id} = Log.get_car_by(vin: "xxxxx")
assert %Summary{state: :suspended} = TeslaMate.Vehicles.summary(id)
conn = put(conn, Routes.car_path(conn, :resume_logging, id))
assert "" == response(conn, 204)
end
end
def start_vehicles(events \\ []) do
{:ok, _pid} = start_supervised({ApiMock, name: :api_vehicle, events: events, pid: self()})
{:ok, _pid} =
start_supervised(
{TeslaMate.Vehicles,
vehicle: VehicleMock,
vehicles: [
%TeslaApi.Vehicle{
display_name: "foo",
id: 4242,
vehicle_id: 404,
vin: "xxxxx"
}
]}
)
:ok
end
end