Files
meshcore-hub/tests/test_web/test_map.py
Louis King 8f660d6b94 Enhance map page with GPS fallback, infrastructure filter, and UI improvements
- Add GPS coordinate fallback: use tag coords, fall back to model coords
- Filter out nodes at (0, 0) coordinates (likely unset defaults)
- Add "Show" filter to toggle between All Nodes and Infrastructure Only
- Add "Show Labels" checkbox (labels hidden by default, appear on hover)
- Infrastructure nodes display network logo instead of emoji
- Add radius-based bounds filtering (20km) to prevent outlier zoom issues
- Position labels underneath pins, centered with transparent background
- Calculate and return infra_center for infrastructure node focus
- Initial map view focuses on infrastructure nodes when available
- Update popup button to outline style
- Add comprehensive tests for new functionality

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-07 20:05:56 +00:00

421 lines
14 KiB
Python

"""Tests for the map page routes."""
from typing import Any
from fastapi.testclient import TestClient
from tests.test_web.conftest import MockHttpClient
class TestMapPage:
"""Tests for the map page."""
def test_map_returns_200(self, client: TestClient) -> None:
"""Test that map page returns 200 status code."""
response = client.get("/map")
assert response.status_code == 200
def test_map_returns_html(self, client: TestClient) -> None:
"""Test that map page returns HTML content."""
response = client.get("/map")
assert "text/html" in response.headers["content-type"]
def test_map_contains_network_name(self, client: TestClient) -> None:
"""Test that map page contains the network name."""
response = client.get("/map")
assert "Test Network" in response.text
def test_map_contains_leaflet(self, client: TestClient) -> None:
"""Test that map page includes Leaflet library."""
response = client.get("/map")
# Should include Leaflet JS/CSS
assert "leaflet" in response.text.lower()
class TestMapDataEndpoint:
"""Tests for the map data JSON endpoint."""
def test_map_data_returns_200(self, client: TestClient) -> None:
"""Test that map data endpoint returns 200 status code."""
response = client.get("/map/data")
assert response.status_code == 200
def test_map_data_returns_json(self, client: TestClient) -> None:
"""Test that map data endpoint returns JSON content."""
response = client.get("/map/data")
assert "application/json" in response.headers["content-type"]
def test_map_data_contains_nodes(
self, client: TestClient, mock_http_client: MockHttpClient
) -> None:
"""Test that map data contains nodes with location."""
response = client.get("/map/data")
data = response.json()
assert "nodes" in data
# The mock includes a node with lat/lon tags
nodes = data["nodes"]
# Should have at least one node with location
assert len(nodes) == 1
assert nodes[0]["name"] == "Node Two"
assert nodes[0]["lat"] == 40.7128
assert nodes[0]["lon"] == -74.0060
def test_map_data_contains_center(
self, client: TestClient, mock_http_client: MockHttpClient
) -> None:
"""Test that map data contains network center location."""
response = client.get("/map/data")
data = response.json()
assert "center" in data
center = data["center"]
assert center["lat"] == 40.7128
assert center["lon"] == -74.0060
def test_map_data_excludes_nodes_without_location(
self, client: TestClient, mock_http_client: MockHttpClient
) -> None:
"""Test that map data excludes nodes without location tags."""
response = client.get("/map/data")
data = response.json()
nodes = data["nodes"]
# Node One has no location tags, so should not appear
node_names = [n["name"] for n in nodes]
assert "Node One" not in node_names
class TestMapDataAPIErrors:
"""Tests for map data handling API errors."""
def test_map_data_handles_api_error(
self, web_app: Any, mock_http_client: MockHttpClient
) -> None:
"""Test that map data handles API errors gracefully."""
mock_http_client.set_response(
"GET", "/api/v1/nodes", status_code=500, json_data=None
)
web_app.state.http_client = mock_http_client
client = TestClient(web_app, raise_server_exceptions=True)
response = client.get("/map/data")
# Should still return 200 with empty nodes
assert response.status_code == 200
data = response.json()
assert data["nodes"] == []
assert "center" in data
class TestMapDataFiltering:
"""Tests for map data location filtering."""
def test_map_data_filters_invalid_lat(
self, web_app: Any, mock_http_client: MockHttpClient
) -> None:
"""Test that map data filters nodes with invalid latitude."""
mock_http_client.set_response(
"GET",
"/api/v1/nodes",
status_code=200,
json_data={
"items": [
{
"id": "node-1",
"public_key": "abc123",
"name": "Bad Lat Node",
"tags": [
{"key": "lat", "value": "not-a-number"},
{"key": "lon", "value": "-74.0060"},
],
},
],
"total": 1,
},
)
web_app.state.http_client = mock_http_client
client = TestClient(web_app, raise_server_exceptions=True)
response = client.get("/map/data")
data = response.json()
# Node with invalid lat should be excluded
assert len(data["nodes"]) == 0
def test_map_data_filters_missing_lon(
self, web_app: Any, mock_http_client: MockHttpClient
) -> None:
"""Test that map data filters nodes with missing longitude."""
mock_http_client.set_response(
"GET",
"/api/v1/nodes",
status_code=200,
json_data={
"items": [
{
"id": "node-1",
"public_key": "abc123",
"name": "No Lon Node",
"tags": [
{"key": "lat", "value": "40.7128"},
],
},
],
"total": 1,
},
)
web_app.state.http_client = mock_http_client
client = TestClient(web_app, raise_server_exceptions=True)
response = client.get("/map/data")
data = response.json()
# Node with only lat should be excluded
assert len(data["nodes"]) == 0
def test_map_data_filters_zero_coordinates(
self, web_app: Any, mock_http_client: MockHttpClient
) -> None:
"""Test that map data filters nodes with (0, 0) coordinates."""
mock_http_client.set_response(
"GET",
"/api/v1/nodes",
status_code=200,
json_data={
"items": [
{
"id": "node-1",
"public_key": "abc123",
"name": "Zero Coord Node",
"lat": 0.0,
"lon": 0.0,
"tags": [],
},
],
"total": 1,
},
)
web_app.state.http_client = mock_http_client
client = TestClient(web_app, raise_server_exceptions=True)
response = client.get("/map/data")
data = response.json()
# Node at (0, 0) should be excluded
assert len(data["nodes"]) == 0
def test_map_data_uses_model_coordinates_as_fallback(
self, web_app: Any, mock_http_client: MockHttpClient
) -> None:
"""Test that map data uses model lat/lon when tags are not present."""
mock_http_client.set_response(
"GET",
"/api/v1/nodes",
status_code=200,
json_data={
"items": [
{
"id": "node-1",
"public_key": "abc123",
"name": "Model Coords Node",
"lat": 51.5074,
"lon": -0.1278,
"tags": [],
},
],
"total": 1,
},
)
web_app.state.http_client = mock_http_client
client = TestClient(web_app, raise_server_exceptions=True)
response = client.get("/map/data")
data = response.json()
# Node should use model coordinates
assert len(data["nodes"]) == 1
assert data["nodes"][0]["lat"] == 51.5074
assert data["nodes"][0]["lon"] == -0.1278
def test_map_data_prefers_tag_coordinates_over_model(
self, web_app: Any, mock_http_client: MockHttpClient
) -> None:
"""Test that tag coordinates take priority over model coordinates."""
mock_http_client.set_response(
"GET",
"/api/v1/nodes",
status_code=200,
json_data={
"items": [
{
"id": "node-1",
"public_key": "abc123",
"name": "Both Coords Node",
"lat": 51.5074,
"lon": -0.1278,
"tags": [
{"key": "lat", "value": "40.7128"},
{"key": "lon", "value": "-74.0060"},
],
},
],
"total": 1,
},
)
web_app.state.http_client = mock_http_client
client = TestClient(web_app, raise_server_exceptions=True)
response = client.get("/map/data")
data = response.json()
# Node should use tag coordinates, not model
assert len(data["nodes"]) == 1
assert data["nodes"][0]["lat"] == 40.7128
assert data["nodes"][0]["lon"] == -74.0060
class TestMapDataInfrastructure:
"""Tests for infrastructure node handling in map data."""
def test_map_data_includes_infra_center(
self, web_app: Any, mock_http_client: MockHttpClient
) -> None:
"""Test that map data includes infrastructure center when infra nodes exist."""
mock_http_client.set_response(
"GET",
"/api/v1/nodes",
status_code=200,
json_data={
"items": [
{
"id": "node-1",
"public_key": "abc123",
"name": "Infra Node",
"lat": 40.0,
"lon": -74.0,
"tags": [{"key": "role", "value": "infra"}],
},
{
"id": "node-2",
"public_key": "def456",
"name": "Regular Node",
"lat": 41.0,
"lon": -75.0,
"tags": [],
},
],
"total": 2,
},
)
web_app.state.http_client = mock_http_client
client = TestClient(web_app, raise_server_exceptions=True)
response = client.get("/map/data")
data = response.json()
# Should have infra_center based on infra node only
assert data["infra_center"] is not None
assert data["infra_center"]["lat"] == 40.0
assert data["infra_center"]["lon"] == -74.0
def test_map_data_infra_center_null_when_no_infra(
self, web_app: Any, mock_http_client: MockHttpClient
) -> None:
"""Test that infra_center is null when no infrastructure nodes exist."""
mock_http_client.set_response(
"GET",
"/api/v1/nodes",
status_code=200,
json_data={
"items": [
{
"id": "node-1",
"public_key": "abc123",
"name": "Regular Node",
"lat": 40.0,
"lon": -74.0,
"tags": [],
},
],
"total": 1,
},
)
web_app.state.http_client = mock_http_client
client = TestClient(web_app, raise_server_exceptions=True)
response = client.get("/map/data")
data = response.json()
assert data["infra_center"] is None
def test_map_data_sets_is_infra_flag(
self, web_app: Any, mock_http_client: MockHttpClient
) -> None:
"""Test that nodes have correct is_infra flag based on role tag."""
mock_http_client.set_response(
"GET",
"/api/v1/nodes",
status_code=200,
json_data={
"items": [
{
"id": "node-1",
"public_key": "abc123",
"name": "Infra Node",
"lat": 40.0,
"lon": -74.0,
"tags": [{"key": "role", "value": "infra"}],
},
{
"id": "node-2",
"public_key": "def456",
"name": "Regular Node",
"lat": 41.0,
"lon": -75.0,
"tags": [{"key": "role", "value": "other"}],
},
],
"total": 2,
},
)
web_app.state.http_client = mock_http_client
client = TestClient(web_app, raise_server_exceptions=True)
response = client.get("/map/data")
data = response.json()
nodes_by_name = {n["name"]: n for n in data["nodes"]}
assert nodes_by_name["Infra Node"]["is_infra"] is True
assert nodes_by_name["Regular Node"]["is_infra"] is False
def test_map_data_debug_includes_infra_count(
self, web_app: Any, mock_http_client: MockHttpClient
) -> None:
"""Test that debug info includes infrastructure node count."""
mock_http_client.set_response(
"GET",
"/api/v1/nodes",
status_code=200,
json_data={
"items": [
{
"id": "node-1",
"public_key": "abc123",
"name": "Infra Node",
"lat": 40.0,
"lon": -74.0,
"tags": [{"key": "role", "value": "infra"}],
},
],
"total": 1,
},
)
web_app.state.http_client = mock_http_client
client = TestClient(web_app, raise_server_exceptions=True)
response = client.get("/map/data")
data = response.json()
assert data["debug"]["infra_nodes"] == 1