HTML-OSM

 view release on metacpan or  search on metacpan

t/function.t  view on Meta::CPAN

	is($m->center([999, 0]), 0, 'out-of-range returns 0');
	ok(!$m->{center}, 'center not set on failure');
};

subtest 'center: geo object with latitude()/longitude() methods' => sub {
	my $m = HTML::OSM->new();
	mock 'MockCtrObj::latitude'  => sub { $CFG{LAT_PARIS} };
	mock 'MockCtrObj::longitude' => sub { $CFG{LON_PARIS} };
	is($m->center(bless {}, 'MockCtrObj'), 1, 'geo object center succeeds');
	is_deeply($m->{center}, [$CFG{LAT_PARIS}, $CFG{LON_PARIS}], 'stored');
};

subtest 'center: string address resolved via geocoder' => sub {
	my $m = HTML::OSM->new();
	mock 'MockCtrGeo::geocode' => sub { { lat => $CFG{LAT_NYC}, lon => $CFG{LON_NYC} } };
	$m->{geocoder} = bless {}, 'MockCtrGeo';
	is($m->center('New York'), 1, 'string center geocoded');
	is_deeply($m->{center}, [$CFG{LAT_NYC}, $CFG{LON_NYC}], 'stored');
};

subtest 'center: geocode failure returns 0' => sub {
	my $m = HTML::OSM->new();
	mock 'MockCtrFail::geocode' => sub { undef };
	$m->{geocoder} = bless {}, 'MockCtrFail';
	is($m->center('Nowhere'), 0, 'geocode failure -> 0');
};

subtest 'center: unknown ref type croaks with message' => sub {
	my $m   = HTML::OSM->new();
	my $bad = bless {}, 'UnknownCenter';
	throws_ok { $m->center($bad) }
		qr/center\(\): unknown point type: UnknownCenter/,
		'exact croak message';
};

# ─────────────────────────────────────────────────────────────────────────────
# add_geojson()
# ─────────────────────────────────────────────────────────────────────────────

subtest 'add_geojson: pre-parsed hashref stored, returns 1' => sub {
	my $m    = HTML::OSM->new();
	my $data = { type => 'FeatureCollection', features => [] };
	is($m->add_geojson($data), 1, 'returns 1');
	is(scalar @{$m->{geojson}}, 1,     'one layer');
	is($m->{geojson}[0]{data},  $data, 'same ref stored');
	returns_ok(1, { type => 'integer' }, 'add_geojson returns integer');
};

subtest 'add_geojson: JSON string is decoded in-place' => sub {
	my $m = HTML::OSM->new();
	$m->add_geojson('{"type":"FeatureCollection","features":[]}');
	is(ref($m->{geojson}[0]{data}), 'HASH', 'decoded to hashref');
	is($m->{geojson}[0]{data}{type}, 'FeatureCollection', 'content correct');
};

subtest 'add_geojson: invalid JSON dies' => sub {
	my $m = HTML::OSM->new();
	dies_ok { $m->add_geojson('not json at all') } 'invalid JSON dies';
};

subtest 'add_geojson: style and popup opts stored' => sub {
	my $m = HTML::OSM->new();
	$m->add_geojson({ type => 'FeatureCollection', features => [] },
		style => { color => '#ff0000' }, popup => 'name');
	is_deeply($m->{geojson}[0]{opts}{style}, { color => '#ff0000' }, 'style stored');
	is($m->{geojson}[0]{opts}{popup}, 'name', 'popup stored');
};

subtest 'add_geojson: multiple calls accumulate layers' => sub {
	my $m = HTML::OSM->new();
	$m->add_geojson({ type => 'FeatureCollection', features => [] });
	$m->add_geojson({ type => 'FeatureCollection', features => [] });
	is(scalar @{$m->{geojson}}, 2, 'two layers');
	memory_cycle_ok($m->{geojson}, 'no cycles in geojson');
};

# ─────────────────────────────────────────────────────────────────────────────
# add_heatmap()
# ─────────────────────────────────────────────────────────────────────────────

subtest 'add_heatmap: valid arrayref stored, returns 1' => sub {
	my $m      = HTML::OSM->new();
	my $points = [[$CFG{LAT_LONDON}, $CFG{LON_LONDON}, 0.8]];
	is($m->add_heatmap($points), 1, 'returns 1');
	is(scalar @{$m->{heatmap_layers}},    1,      'one layer');
	is($m->{heatmap_layers}[0]{points},   $points, 'same ref stored');
	returns_ok(1, { type => 'integer' }, 'returns integer');
};

subtest 'add_heatmap: radius and blur opts stored' => sub {
	my $m = HTML::OSM->new();
	$m->add_heatmap([[0, 0]], radius => 30, blur => 20);
	is($m->{heatmap_layers}[0]{opts}{radius}, 30, 'radius stored');
	is($m->{heatmap_layers}[0]{opts}{blur},   20, 'blur stored');
};

subtest 'add_heatmap: non-arrayref croaks with exact message' => sub {
	my $m = HTML::OSM->new();
	throws_ok { $m->add_heatmap('string') }
		qr/add_heatmap: points must be an arrayref/,
		'string croaks';
	throws_ok { $m->add_heatmap({ lat => 1 }) }
		qr/add_heatmap: points must be an arrayref/,
		'hashref croaks';
	throws_ok { $m->add_heatmap(undef) }
		qr/add_heatmap: points must be an arrayref/,
		'undef croaks';
};

subtest 'add_heatmap: multiple layers accumulate' => sub {
	my $m = HTML::OSM->new();
	$m->add_heatmap([[0, 0]]);
	$m->add_heatmap([[1, 1]]);
	is(scalar @{$m->{heatmap_layers}}, 2, 'two layers');
	memory_cycle_ok($m->{heatmap_layers}, 'no cycles in heatmap_layers');
};

# ─────────────────────────────────────────────────────────────────────────────
# add_gpx()
# ─────────────────────────────────────────────────────────────────────────────

subtest 'add_gpx: URL stored, returns 1' => sub {
	my $m   = HTML::OSM->new();
	my $url = 'https://example.com/track.gpx';
	is($m->add_gpx($url), 1, 'returns 1');
	is(scalar @{$m->{gpx_tracks}}, 1,    'one track');

t/function.t  view on Meta::CPAN

	my (undef, $body) = $m->onload_render();
	like($body, qr/search-box/,          'search box present');
	like($body, qr/reset-button/,        'reset button present');
	like($body, qr/clear-search-button/, 'clear-search button present');
	like($body, qr/searchMarkers/,        'searchMarkers array present');
};

subtest 'onload_render: heatmap plugin injected only when needed' => sub {
	my $with = HTML::OSM->new();
	$with->add_marker([$CFG{LAT_LONDON}, $CFG{LON_LONDON}]);
	$with->add_heatmap([[$CFG{LAT_LONDON}, $CFG{LON_LONDON}, 0.9]]);
	like(($with->onload_render())[0], qr/leaflet.heat/i, 'plugin in head when used');

	my $without = HTML::OSM->new();
	$without->add_marker([$CFG{LAT_LONDON}, $CFG{LON_LONDON}]);
	unlike(($without->onload_render())[0], qr/leaflet.heat/i, 'plugin absent when unused');
};

subtest 'onload_render: GPX plugin injected only when needed' => sub {
	my $with = HTML::OSM->new();
	$with->add_marker([$CFG{LAT_LONDON}, $CFG{LON_LONDON}]);
	$with->add_gpx('https://example.com/track.gpx');
	like(($with->onload_render())[0], qr/gpx/i, 'GPX plugin in head when used');

	my $without = HTML::OSM->new();
	$without->add_marker([$CFG{LAT_LONDON}, $CFG{LON_LONDON}]);
	unlike(($without->onload_render())[0], qr/leaflet-gpx/i, 'GPX plugin absent when unused');
};

subtest 'onload_render: cluster => 1 injects all three cluster assets' => sub {
	my $m = HTML::OSM->new(cluster => 1);
	$m->add_marker([$CFG{LAT_LONDON}, $CFG{LON_LONDON}]);
	$m->add_marker([$CFG{LAT_PARIS},  $CFG{LON_PARIS}]);
	my ($head, $body) = $m->onload_render();
	like($head, qr/markercluster.*\.js/i,        'cluster JS');
	like($head, qr/MarkerCluster\.css/,           'cluster CSS');
	like($head, qr/MarkerCluster\.Default\.css/,  'cluster default CSS');
	like($body, qr/markerClusterGroup/,            'clusterGroup created');
	like($body, qr/clusterGroup\.addLayer/,        'markers via addLayer');
	like($body, qr/map\.addLayer\(clusterGroup\)/, 'clusterGroup added to map');
};

subtest 'onload_render: without cluster, markers added directly' => sub {
	my $m = HTML::OSM->new();
	$m->add_marker([$CFG{LAT_LONDON}, $CFG{LON_LONDON}]);
	my ($head, $body) = $m->onload_render();
	unlike($head, qr/markercluster/, 'no cluster assets');
	unlike($body, qr/clusterGroup/,  'no clusterGroup');
	like($body,   qr/addTo\(map\)/,  'marker direct to map');
};

subtest 'onload_render: icon URL uses L.icon in emitted JS' => sub {
	my $m = HTML::OSM->new();
	$m->add_marker([$CFG{LAT_LONDON}, $CFG{LON_LONDON}],
		icon => 'https://example.com/pin.png');
	my (undef, $body) = $m->onload_render();
	like($body, qr/L\.icon/,              'L.icon call present');
	like($body, qr|example\.com/pin\.png|, 'icon URL present');
};

subtest 'onload_render: popup label with single quote is JS-escaped' => sub {
	# A raw single quote in a label would break the JS string — _js_string must fire.
	my $m = HTML::OSM->new();
	$m->add_marker([$CFG{LAT_LONDON}, $CFG{LON_LONDON}], html => "O'Brien's Pub");
	my (undef, $body) = $m->onload_render();
	like($body, qr/O\\'Brien/, 'single quote JS-escaped in popup label');
};

subtest 'onload_render: GPX URL with special chars is JS-escaped' => sub {
	my $m = HTML::OSM->new();
	$m->add_marker([$CFG{LAT_LONDON}, $CFG{LON_LONDON}]);
	$m->add_gpx("https://example.com/track's.gpx");
	my (undef, $body) = $m->onload_render();
	like($body, qr/track\\'s\.gpx/, 'GPX URL JS-escaped');
};

subtest 'onload_render: GeoJSON style colour appears in body' => sub {
	my $m = HTML::OSM->new();
	$m->add_marker([$CFG{LAT_LONDON}, $CFG{LON_LONDON}]);
	$m->add_geojson({ type => 'FeatureCollection', features => [] },
		style => { color => '#abcdef' });
	my (undef, $body) = $m->onload_render();
	like($body, qr/L\.geoJSON/, 'L.geoJSON call present');
	like($body, qr/#abcdef/,    'style colour present');
};

subtest 'onload_render: choropleth emits colour lookup and fillColor' => sub {
	my $m = HTML::OSM->new();
	$m->center([$CFG{LAT_LONDON}, $CFG{LON_LONDON}]);
	$m->add_choropleth(\@FEATURES, { England => 100, Scotland => 50 }, key => 'name');
	my (undef, $body) = $m->onload_render();
	like($body, qr/choroplethColors/, 'colour lookup emitted');
	like($body, qr/fillColor/,        'fillColor style emitted');
	like($body, qr/choroplethValues/, 'value lookup emitted');
};

subtest 'onload_render: non-marker-only render with center succeeds' => sub {
	my $m = HTML::OSM->new();
	$m->center([$CFG{LAT_LONDON}, $CFG{LON_LONDON}]);
	$m->add_geojson({ type => 'FeatureCollection', features => [] });
	my @r;
	lives_ok { @r = $m->onload_render() } 'lives with center + GeoJSON only';
	is(scalar @r, 2, 'two-element result');
};

# ─────────────────────────────────────────────────────────────────────────────
# _fetch_coordinates()
# White-box via direct method call; all HTTP is intercepted by the global mock.
# ─────────────────────────────────────────────────────────────────────────────

subtest '_fetch_coordinates: empty string croaks' => sub {
	my $m = HTML::OSM->new();
	throws_ok { $m->_fetch_coordinates('') }
		qr/address not given to _fetch_coordinates/,
		'empty string croaks';
};

subtest '_fetch_coordinates: undef croaks' => sub {
	my $m = HTML::OSM->new();
	throws_ok { $m->_fetch_coordinates(undef) }
		qr/address not given to _fetch_coordinates/,
		'undef croaks';
};

subtest '_fetch_coordinates: geocoder returns blessed object with lat/lon methods' => sub {
	my $m = HTML::OSM->new();



( run in 0.631 second using v1.01-cache-2.11-cpan-364913b4093 )