Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
309 changes: 309 additions & 0 deletions tests/test_foliumap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
"""Tests for the foliumap module."""

import json
import os
import tempfile
import unittest
from unittest import mock

try:
import sys
import folium

_fake_basemaps = {
"ROADMAP": folium.TileLayer(
tiles="https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}",
attr="Google",
name="ROADMAP",
),
"SATELLITE": folium.TileLayer(
tiles="https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}",
attr="Google",
name="SATELLITE",
),
"HYBRID": folium.TileLayer(
tiles="https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}",
attr="Google",
name="HYBRID",
),
"TERRAIN": folium.TileLayer(
tiles="https://mt1.google.com/vt/lyrs=p&x={x}&y={y}&z={z}",
attr="Google",
name="TERRAIN",
),
"OpenStreetMap": folium.TileLayer(
tiles="OpenStreetMap", attr="OSM", name="OpenStreetMap"
),
}

# Importing the geemap package causes `from .geemap import *` which brings
# a Box named `basemaps` into the geemap namespace, shadowing the real
# basemaps *module*. When foliumap.py later does `from . import basemaps`
# Python resolves the package attribute (the Box) instead of the module,
# so `basemaps.xyz_to_folium()` fails.
#
# Fix: ensure the geemap package is loaded, then temporarily restore the
# real basemaps module as the package attribute so foliumap's
# `from . import basemaps` picks up the actual module (with our mock on it).
import geemap

_real_basemaps_module = sys.modules["geemap.basemaps"]

# Drop any cached foliumap so the module-level code re-executes with our mock.
sys.modules.pop("geemap.foliumap", None)

_saved_attr = getattr(geemap, "basemaps", None)
geemap.basemaps = _real_basemaps_module

with mock.patch.object(
_real_basemaps_module, "xyz_to_folium", return_value=_fake_basemaps
):
from geemap import foliumap

geemap.basemaps = _saved_attr
FOLIUM_AVAILABLE = True
except Exception:
FOLIUM_AVAILABLE = False


@unittest.skipUnless(FOLIUM_AVAILABLE, "folium not available")
class FoliumapTest(unittest.TestCase):

def _make_map(self, **kwargs) -> foliumap.Map:
return foliumap.Map(ee_initialize=False, **kwargs)

def test_map_init_default_params(self):
m = self._make_map()
self.assertIsInstance(m, folium.Map)
self.assertEqual(m.location, [20, 0])
self.assertEqual(m.options["zoom"], 2)

def test_map_init_custom_center(self):
m = self._make_map(center=[40, -100])
self.assertEqual(m.location, [40, -100])

def test_map_init_custom_zoom(self):
m = self._make_map(zoom=10)
self.assertEqual(m.options["zoom"], 10)

def test_map_init_location_param(self):
m = self._make_map(location=[35, 139])
self.assertEqual(m.location, [35, 139])

def test_map_baseclass(self):
m = self._make_map()
self.assertEqual(m.baseclass, "folium")

def test_map_init_draw_features_empty(self):
m = self._make_map()
self.assertEqual(m.draw_features, [])
self.assertIsNone(m.draw_last_feature)

def test_map_max_zoom_default(self):
m = self._make_map()
# The default fit_bounds call uses maxZoom; verify via the FitBounds child.
fit_children = [
c for c in m._children.values() if type(c).__name__ == "FitBounds"
]
self.assertTrue(len(fit_children) > 0)
self.assertEqual(fit_children[0].options.get("maxZoom"), 2)

def test_set_center_updates_bounds(self):
m = self._make_map()
m.set_center(-122.4, 37.8, zoom=12)
# folium stores bounds via FitBounds children, not in get_bounds().
fit_children = [
c for c in m._children.values() if type(c).__name__ == "FitBounds"
]
last = fit_children[-1]
self.assertAlmostEqual(last.bounds[0][0], 37.8, places=1)
self.assertAlmostEqual(last.bounds[0][1], -122.4, places=1)

def test_zoom_to_bounds_updates_bounds(self):
m = self._make_map()
m.zoom_to_bounds([-122.5, 37.5, -122.0, 38.0])
# folium stores bounds via FitBounds children.
fit_children = [
c for c in m._children.values() if type(c).__name__ == "FitBounds"
]
last = fit_children[-1]
south, west = last.bounds[0]
north, east = last.bounds[1]
self.assertAlmostEqual(south, 37.5, places=1)
self.assertAlmostEqual(west, -122.5, places=1)
self.assertAlmostEqual(north, 38.0, places=1)
self.assertAlmostEqual(east, -122.0, places=1)

def test_add_tile_layer_increases_children(self):
m = self._make_map()
children_before = len(m._children)
m.add_tile_layer(
tiles="https://tile.example.com/{z}/{x}/{y}.png",
name="Test Layer",
attribution="Test",
)
self.assertGreater(len(m._children), children_before)

def test_add_tile_layer_name_in_children(self):
m = self._make_map()
m.add_tile_layer(
tiles="https://tile.example.com/{z}/{x}/{y}.png",
name="MyTiles",
attribution="Test",
)
child_names = [
getattr(c, "tile_name", getattr(c, "name", ""))
for c in m._children.values()
]
# The tile layer name should appear in children tile_name attributes.
self.assertIn("MyTiles", child_names)

def test_add_wms_layer_increases_children(self):
m = self._make_map()
children_before = len(m._children)
m.add_wms_layer(
url="https://example.com/wms",
layers="test_layer",
name="WMS Test",
)
self.assertGreater(len(m._children), children_before)

def test_add_marker_list(self):
m = self._make_map()
children_before = len(m._children)
m.add_marker(location=[37.8, -122.4], popup="Test")
self.assertGreater(len(m._children), children_before)

def test_add_marker_tuple(self):
m = self._make_map()
children_before = len(m._children)
m.add_marker(location=(37.8, -122.4), tooltip="Tooltip")
self.assertGreater(len(m._children), children_before)

def test_add_marker_invalid_type_raises(self):
m = self._make_map()
with self.assertRaises(TypeError):
m.add_marker(location="invalid")

def test_add_geojson_from_dict(self):
m = self._make_map()
geojson_data = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-122.4, 37.8],
},
"properties": {"name": "Test Point"},
}
],
}
children_before = len(m._children)
m.add_geojson(geojson_data, layer_name="GeoJSON Test")
self.assertGreater(len(m._children), children_before)

def test_add_geojson_from_file(self):
geojson_data = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-122.4, 37.8],
},
"properties": {"name": "Test"},
}
],
}
with tempfile.TemporaryDirectory() as tmpdir:
filepath = os.path.join(tmpdir, "test.geojson")
with open(filepath, "w") as f:
json.dump(geojson_data, f)
m = self._make_map()
children_before = len(m._children)
m.add_geojson(filepath, layer_name="File GeoJSON")
self.assertGreater(len(m._children), children_before)

def test_add_geojson_file_not_found_raises(self):
m = self._make_map()
with self.assertRaises(Exception):
m.add_geojson("/nonexistent/path.geojson")

def test_add_geojson_invalid_type_raises(self):
m = self._make_map()
with self.assertRaises(Exception):
m.add_geojson(12345)

def test_add_heatmap_from_list(self):
m = self._make_map()
data = [[37.8, -122.4, 1.0], [37.9, -122.3, 2.0]]
children_before = len(m._children)
m.add_heatmap(data=data, name="Heat")
self.assertGreater(len(m._children), children_before)

def test_add_heatmap_invalid_data_raises(self):
m = self._make_map()
with self.assertRaises(ValueError):
m.add_heatmap(data=12345)

def test_to_html_returns_string(self):
m = self._make_map()
html = m.to_html()
self.assertIsInstance(html, str)
self.assertIn("<html>", html.lower())
self.assertIn("leaflet", html.lower())

def test_to_html_saves_file(self):
with tempfile.TemporaryDirectory() as tmpdir:
filepath = os.path.join(tmpdir, "test_map.html")
m = self._make_map()
m.to_html(filepath)
self.assertTrue(os.path.exists(filepath))
with open(filepath) as f:
content = f.read()
self.assertIn("<html>", content.lower())
self.assertIn("leaflet", content.lower())

def test_to_html_invalid_extension_raises(self):
m = self._make_map()
with self.assertRaises(ValueError):
m.to_html("output.txt")

def test_add_layer_control(self):
m = self._make_map()
children_before = len(m._children)
m.add_layer_control()
self.assertGreater(len(m._children), children_before)

def test_set_control_visibility_adds_children(self):
m = self._make_map()
children_before = len(m._children)
m.set_control_visibility(
layerControl=True,
fullscreenControl=True,
latLngPopup=True,
)
self.assertGreater(len(m._children), children_before)

def test_set_options_adds_basemap(self):
m = self._make_map()
children_before = len(m._children)
m.setOptions("HYBRID")
self.assertGreater(len(m._children), children_before)

def test_set_options_invalid_basemap_raises(self):
m = self._make_map()
with self.assertRaises(Exception):
m.setOptions("INVALID_BASEMAP_XYZ")

def test_add_cog_mosaic_raises_not_implemented(self):
m = self._make_map()
with self.assertRaises(NotImplementedError):
m.add_cog_mosaic()


if __name__ == "__main__":
unittest.main()