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 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, "")) { |n| n["trackPoints"] if n["trackPoints"].is_a?(Array) } assert_nil points end end