Files
maptool/test/test_livetrack_client.rb
T
randy b0c93d10d7
CI / check (push) Successful in 18s
CI / deploy (push) Successful in 12s
Detect a LiveTrack session ending the moment Garmin confirms it, not 30min later
Watched the session from the last two commits actually get saved:
Garmin updated its `end` field from the future 24h placeholder to
within a second of the last point's own time, immediately — much
faster than the recency-of-last-point fallback alone would have
noticed (that only flips after ACTIVE_WITHIN with no new points, so a
just-saved session with a last point seconds old would still read as
active for up to 30 more minutes).

Now checks end-already-in-the-past first (authoritative and near
instant when Garmin has set it), falling back to point recency only
when end still shows the future placeholder — the case an abandoned,
never-saved session never updates.
2026-09-20 16:55:57 +02:00

104 lines
5.7 KiB
Ruby

require_relative "test_helper"
require "maptool/livetrack_client"
# No real network calls here — Garmin's page format was verified manually
# against real sessions during development; a synthetic fixture (matching
# the real page's structure: several `self.__next_f.push(...)` calls, one
# holding the track points nested under some data-fetching library's own
# structure) is what actually needs to keep working over time.
class LiveTrackClientTest < Minitest::Test
def setup
@client = Maptool::LiveTrackClient.new
end
FAKE_PAGE = <<~HTML.freeze
<html><body>
<script>self.__next_f.push([1,"6:[\\"$\\",\\"div\\",null,{}]"])</script>
<script>self.__next_f.push([1,"9:{\\"session\\":{\\"sessionName\\":\\"Test Ride\\",\\"start\\":\\"2026-01-01T00:00:00Z\\",\\"end\\":\\"2026-01-01T01:00:00Z\\"}}"])</script>
<script>self.__next_f.push([1,"10:{\\"unrelated\\":true,\\"nested\\":{\\"data\\":{\\"pages\\":[{\\"trackPoints\\":[{\\"position\\":{\\"lat\\":48.1,\\"lon\\":17.2},\\"dateTime\\":\\"2026-01-01T00:00:00Z\\",\\"totalDistanceMeters\\":\\"$undefined\\"},{\\"position\\":{\\"lat\\":48.2,\\"lon\\":17.3},\\"dateTime\\":\\"2026-01-01T00:01:00Z\\",\\"activityType\\":\\"CYCLING\\",\\"totalDistanceMeters\\":5000,\\"totalDurationSecs\\":600}]}]}}}"])</script>
<script>self.__next_f.push([1,"11:{\\"courses\\":[{\\"name\\":\\"40 km okruh\\",\\"geoPoints\\":[{\\"position\\":{\\"lat\\":48.05,\\"lon\\":17.35},\\"distanceMeters\\":0},{\\"position\\":{\\"lat\\":48.06,\\"lon\\":17.36},\\"distanceMeters\\":1000}]}]}"])</script>
</body></html>
HTML
def test_finds_track_points_and_session_across_separate_push_payloads
payloads = @client.send(:parse_payloads, FAKE_PAGE)
points = @client.send(:search, payloads) { |n| n["trackPoints"] if n["trackPoints"].is_a?(Array) }
assert_equal 2, points.size
assert_in_delta 48.1, points[0]["position"]["lat"]
session = @client.send(:search, payloads) { |n| n if n["sessionName"] }
assert_equal "Test Ride", session["sessionName"]
end
def test_undefined_sentinel_is_cleaned_to_nil
payloads = @client.send(:parse_payloads, FAKE_PAGE)
points = @client.send(:search, payloads) { |n| n["trackPoints"] if n["trackPoints"].is_a?(Array) }
cleaned = @client.send(:clean, points)
assert_nil cleaned[0]["totalDistanceMeters"]
end
def test_summarize_pulls_from_last_point_and_session
payloads = @client.send(:parse_payloads, FAKE_PAGE)
points = @client.send(:clean, @client.send(:search, payloads) { |n| n["trackPoints"] if n["trackPoints"].is_a?(Array) })
session = @client.send(:clean, @client.send(:search, payloads) { |n| n if n["sessionName"] })
result = Maptool::LiveTrackClient::Result.new(points: points, session: session)
summary = Maptool::LiveTrackClient.summarize(result)
assert_equal "Test Ride", summary["session_name"]
assert_equal "CYCLING", summary["activity_type"]
assert_equal 5000, summary["distance_meters"]
refute summary["active"], "last track point is long in the past"
end
# Not session["end"].nil? (the original signal, before 2026-09-20 — see
# LiveTrackClient's own comment): verified live against a real,
# still-recording session that day, Garmin had already set `end` to
# exactly start + 24h (the share link's own expiry, not an end time),
# so that check called an active session "ended".
def test_summarize_marks_active_when_last_point_is_recent
payloads = @client.send(:parse_payloads, FAKE_PAGE)
points = @client.send(:clean, @client.send(:search, payloads) { |n| n["trackPoints"] if n["trackPoints"].is_a?(Array) })
points[-1] = points[-1].merge("dateTime" => Time.now.utc.iso8601)
# end present and in the future, same shape Garmin actually sends for a
# session that's still live — active must come from the point, not this.
session = @client.send(:clean, @client.send(:search, payloads) { |n| n if n["sessionName"] })
.merge("end" => (Time.now.utc + 24 * 60 * 60).iso8601)
result = Maptool::LiveTrackClient::Result.new(points: points, session: session)
assert Maptool::LiveTrackClient.summarize(result)["active"]
end
# Verified live the same day: Garmin updates a real session's `end` from
# the future placeholder to within a second of the last point's own
# time immediately once the activity is saved on the watch — so a fresh
# last point alone isn't enough, `end` already being in the past has to
# win even then.
def test_summarize_marks_inactive_once_end_is_in_the_past_even_with_a_fresh_point
payloads = @client.send(:parse_payloads, FAKE_PAGE)
points = @client.send(:clean, @client.send(:search, payloads) { |n| n["trackPoints"] if n["trackPoints"].is_a?(Array) })
points[-1] = points[-1].merge("dateTime" => Time.now.utc.iso8601)
session = @client.send(:clean, @client.send(:search, payloads) { |n| n if n["sessionName"] })
.merge("end" => (Time.now.utc - 1).iso8601)
result = Maptool::LiveTrackClient::Result.new(points: points, session: session)
refute Maptool::LiveTrackClient.summarize(result)["active"]
end
def test_finds_the_planned_course_alongside_the_actual_track
payloads = @client.send(:parse_payloads, FAKE_PAGE)
course = @client.send(:clean, @client.send(:search, payloads) { |n| n["courses"].first if n["courses"].is_a?(Array) && !n["courses"].empty? })
assert_equal "40 km okruh", course["name"]
assert_equal 2, course["geoPoints"].size
assert_in_delta 48.06, course["geoPoints"].last["position"]["lat"]
end
def test_no_track_points_found_in_a_page_without_them
points = @client.send(:search, @client.send(:parse_payloads, "<html></html>")) { |n| n["trackPoints"] if n["trackPoints"].is_a?(Array) }
assert_nil points
end
end