HTML-OSM

 view release on metacpan or  search on metacpan

t/unit.t  view on Meta::CPAN

	is($m->add_marker([]), 0, 'empty -> 0');
};

subtest 'add_marker: single-element arrayref treated as address, geocoder called' => sub {
	mock 'UnitSingleGeo::geocode' => sub { { lat => $C{LAT_PARIS}, lon => $C{LON_PARIS} } };
	my $m = HTML::OSM->new(geocoder => bless({}, 'UnitSingleGeo'));
	is($m->add_marker(['Paris, France']), 1, '["addr"] geocoded -> 1');
};

subtest 'add_marker: geo object with latitude/longitude methods returns 1' => sub {
	mock 'UnitGeoObj::latitude'  => sub { $C{LAT_NYC} };
	mock 'UnitGeoObj::longitude' => sub { $C{LON_NYC} };
	my $m = HTML::OSM->new();
	is($m->add_marker(bless({}, 'UnitGeoObj')), 1, 'geo object -> 1');
};

subtest 'add_marker: string address resolved by geocoder returns 1' => sub {
	mock 'UnitStrGeo::geocode' => sub { { lat => $C{LAT_PARIS}, lon => $C{LON_PARIS} } };
	my $m = HTML::OSM->new(geocoder => bless({}, 'UnitStrGeo'));
	is($m->add_marker('Paris, France', html => 'Paris'), 1, 'string address -> 1');
};

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

# POD MESSAGES: "add_marker(): unknown point type"
subtest 'add_marker: unknown ref type croaks with exact POD message' => sub {
	my $m   = HTML::OSM->new();
	my $bad = bless {}, 'UnitUnknownRef';
	throws_ok { $m->add_marker($bad) }
		qr/add_marker\(\): unknown point type: UnitUnknownRef/,
		'exact croak per POD MESSAGES table';
};

# Verify the markers actually appear in the rendered HTML (not just stored internally).
subtest 'add_marker: multiple markers appear in rendered body' => sub {
	my $m = HTML::OSM->new();
	$m->add_marker([$C{LAT_LONDON}, $C{LON_LONDON}], html => 'London');
	$m->add_marker([$C{LAT_PARIS},  $C{LON_PARIS}],  html => 'Paris');
	my (undef, $body) = $m->onload_render();
	like($body, qr/$C{LAT_LONDON}/, 'London lat in body');
	like($body, qr/$C{LAT_PARIS}/,  'Paris lat in body');
	# Match only markers with numeric coordinates — the search handler uses
	# variable names [lat, lon] not literals, so it won't match.
	my @calls = ($body =~ /L\.marker\(\[-?\d/g);
	is(scalar @calls, 2, 'exactly two L.marker calls with numeric coords emitted');
};

subtest 'add_marker: out-of-range marker does not appear in rendered body' => sub {
	my $m = HTML::OSM->new();
	$m->add_marker([$C{LAT_LONDON}, $C{LON_LONDON}]);      # valid — provides center
	{ local $SIG{__WARN__} = $SILENCE;
	  $m->add_marker([999, 999], html => 'ImpossiblePlace'); }
	my (undef, $body) = $m->onload_render();
	unlike($body, qr/ImpossiblePlace/, 'invalid label absent from body');
};

subtest 'add_marker: html popup label appears in bindPopup in rendered body' => sub {
	my $m = HTML::OSM->new();
	$m->add_marker([$C{LAT_LONDON}, $C{LON_LONDON}], html => 'My London Label');
	my (undef, $body) = $m->onload_render();
	like($body, qr/My London Label/, 'popup label in bindPopup');
};

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

subtest 'add_marker: does not clobber $@ on success' => sub {
	my $m = HTML::OSM->new();
	eval { die 'sentinel' };
	my $before = $@;
	$m->add_marker([$C{LAT_LONDON}, $C{LON_LONDON}]);
	is($@, $before, 'add_marker preserves $@');
};

# ─────────────────────────────────────────────────────────────────────────────
# center()
# POD: returns 1 on success, 0 if point cannot be resolved.
# ─────────────────────────────────────────────────────────────────────────────

subtest 'center: [lat, lon] arrayref returns 1' => sub {
	my $m = HTML::OSM->new();
	my $r = $m->center([$C{LAT_LONDON}, $C{LON_LONDON}]);
	is($r, 1, 'success -> 1');
	returns_ok($r, { type => 'integer' }, 'return type is integer');
};

# POD MESSAGES: "center(): usage: point => [lat, lon]"
subtest 'center: no args dies mentioning center (POD MESSAGES)' => sub {
	my $m = HTML::OSM->new();
	throws_ok { $m->center() } qr/center/i, 'no-arg croak mentions center';
};

# POD MESSAGES: "center(): point must have latitude & longitude"
subtest 'center: one-element arrayref croaks with exact POD message' => sub {
	my $m = HTML::OSM->new();
	throws_ok { $m->center([51.5]) }
		qr/center\(\): point must have latitude and longitude/,
		'wrong-length croak per POD MESSAGES table';
};

subtest 'center: three-element arrayref croaks with exact POD message' => sub {
	my $m = HTML::OSM->new();
	throws_ok { $m->center([51.5, -0.1, 0]) }
		qr/center\(\): point must have latitude and longitude/,
		'three-element croak per POD MESSAGES table';
};

subtest 'center: out-of-range coords return 0' => sub {
	my $m = HTML::OSM->new();
	local $SIG{__WARN__} = $SILENCE;
	my $r = $m->center([999, 0]);
	is($r, 0, 'OOR -> 0');
	returns_ok($r, { type => 'integer' }, 'return type is integer');
};

subtest 'center: geo object with latitude/longitude methods returns 1' => sub {

t/unit.t  view on Meta::CPAN

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

# Verify that center() actually drives the setView() lat/lon in rendered output.
subtest 'center: set value appears in rendered setView lat' => sub {
	my $m = HTML::OSM->new();
	$m->center([$C{LAT_NYC}, $C{LON_NYC}]);
	$m->add_marker([$C{LAT_LONDON}, $C{LON_LONDON}]);
	my (undef, $body) = $m->onload_render();
	like($body, qr/setView\(\[$C{LAT_NYC}/, 'explicit center lat in setView');
};

# POD PSEUDOCODE step 3: "caller-supplied > computed midpoint"
subtest 'center: explicit center overrides marker midpoint' => sub {
	my $m = HTML::OSM->new();
	$m->add_marker([10, 20]);
	$m->add_marker([-10, -20]);   # midpoint would be (0, 0)
	$m->center([$C{LAT_NYC}, $C{LON_NYC}]);
	my (undef, $body) = $m->onload_render();
	like($body,   qr/setView\(\[$C{LAT_NYC}/, 'explicit center wins');
	unlike($body, qr/setView\(\[0, 0\]/,      'midpoint NOT used');
};

# ─────────────────────────────────────────────────────────────────────────────
# add_geojson()
# POD: returns 1 on success; first arg may be hashref, arrayref, or JSON string.
# ─────────────────────────────────────────────────────────────────────────────

subtest 'add_geojson: pre-parsed hashref returns 1' => sub {
	my $m = HTML::OSM->new();
	my $r = $m->add_geojson({ type => 'FeatureCollection', features => [] });
	is($r, 1, 'success -> 1');
	returns_ok($r, { type => 'integer' }, 'return type is integer');
};

subtest 'add_geojson: JSON string returns 1' => sub {
	my $m = HTML::OSM->new();
	is($m->add_geojson('{"type":"FeatureCollection","features":[]}'), 1, 'JSON string -> 1');
};

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

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

subtest 'add_geojson: popup property name appears in rendered body' => sub {
	my $m = HTML::OSM->new();
	$m->add_marker([$C{LAT_LONDON}, $C{LON_LONDON}]);
	$m->add_geojson({ type => 'FeatureCollection', features => [] }, popup => 'region');
	my (undef, $body) = $m->onload_render();
	like($body, qr/region/, "popup property 'region' in body JS");
};

subtest 'add_geojson: multiple layers all rendered' => sub {
	my $m = HTML::OSM->new();
	$m->add_marker([$C{LAT_LONDON}, $C{LON_LONDON}]);
	$m->add_geojson({ type => 'FeatureCollection', features => [] }, style => { color => '#ff0000' });
	$m->add_geojson({ type => 'FeatureCollection', features => [] }, style => { color => '#0000ff' });
	my (undef, $body) = $m->onload_render();
	my @calls = ($body =~ /L\.geoJSON/g);
	is(scalar @calls, 2, 'two L.geoJSON calls emitted');
	like($body, qr/#ff0000/, 'first layer colour');
	like($body, qr/#0000ff/, 'second layer colour');
};

# ─────────────────────────────────────────────────────────────────────────────
# add_heatmap()
# POD: returns 1; croaks with exact message if points not arrayref.
# ─────────────────────────────────────────────────────────────────────────────

subtest 'add_heatmap: arrayref of points returns 1' => sub {
	my $m = HTML::OSM->new();
	my $r = $m->add_heatmap([[$C{LAT_LONDON}, $C{LON_LONDON}, 0.8]]);
	is($r, 1, 'success -> 1');
	returns_ok($r, { type => 'integer' }, 'return type is integer');
};

# POD MESSAGES: "add_heatmap: points must be arrayref"
subtest 'add_heatmap: string croaks with exact POD message' => sub {
	my $m = HTML::OSM->new();
	throws_ok { $m->add_heatmap('not an arrayref') }
		qr/add_heatmap: points must be an arrayref/,
		'string croak per POD MESSAGES table';
};

subtest 'add_heatmap: hashref croaks with exact POD message' => sub {
	my $m = HTML::OSM->new();
	throws_ok { $m->add_heatmap({ a => 1 }) }
		qr/add_heatmap: points must be an arrayref/,
		'hashref croak per POD MESSAGES table';
};

subtest 'add_heatmap: undef croaks with exact POD message' => sub {
	my $m = HTML::OSM->new();
	throws_ok { $m->add_heatmap(undef) }
		qr/add_heatmap: points must be an arrayref/,
		'undef croak per POD MESSAGES table';
};

# Verify that radius/blur opts are propagated to the rendered JS output.
subtest 'add_heatmap: default radius and blur appear in rendered body' => sub {
	my $m = HTML::OSM->new();
	$m->add_marker([$C{LAT_LONDON}, $C{LON_LONDON}]);
	$m->add_heatmap([[$C{LAT_LONDON}, $C{LON_LONDON}]]);
	my (undef, $body) = $m->onload_render();
	like($body, qr/L\.heatLayer/, 'L.heatLayer call present');
	like($body, qr/radius:\s*25/, 'default radius 25');
	like($body, qr/blur:\s*15/,   'default blur 15');
};

subtest 'add_heatmap: custom radius and blur appear in rendered body' => sub {

t/unit.t  view on Meta::CPAN

	like($body, qr/search-box/,               'search-box element present');
	like($body, qr/Enter location/i,           'search box placeholder text present');
};

# POD description: "A 'Clear search markers' button"
subtest 'onload_render: body contains Clear search markers button' => sub {
	my $m = HTML::OSM->new();
	$m->add_marker([$C{LAT_LONDON}, $C{LON_LONDON}]);
	my (undef, $body) = $m->onload_render();
	like($body, qr/clear-search-button/,       'clear-search-button ID present');
	like($body, qr/Clear search markers/i,     'button label text present');
};

# POD description: "A 'Reset Map' button"
subtest 'onload_render: body contains Reset Map button' => sub {
	my $m = HTML::OSM->new();
	$m->add_marker([$C{LAT_LONDON}, $C{LON_LONDON}]);
	my (undef, $body) = $m->onload_render();
	like($body, qr/reset-button/,              'reset-button ID present');
	like($body, qr/Reset Map/i,                'reset button label text present');
};

subtest 'onload_render: body contains searchMarkers array' => sub {
	my $m = HTML::OSM->new();
	$m->add_marker([$C{LAT_LONDON}, $C{LON_LONDON}]);
	my (undef, $body) = $m->onload_render();
	like($body, qr/searchMarkers/, 'searchMarkers variable in body');
};

# Midpoint computation: two equidistant points → center at (0, 0).
subtest 'onload_render: center auto-computed as midpoint of marker bounds' => sub {
	my $m = HTML::OSM->new();
	$m->add_marker([10, 20]);
	$m->add_marker([-10, -20]);
	my (undef, $body) = $m->onload_render();
	like($body, qr/setView\(\[0, 0\]/, 'midpoint (0, 0) used as center');
};

subtest 'onload_render: cluster => 1 wraps markers in clusterGroup' => sub {
	my $m = HTML::OSM->new(cluster => 1);
	$m->add_marker([$C{LAT_LONDON}, $C{LON_LONDON}]);
	$m->add_marker([$C{LAT_PARIS},  $C{LON_PARIS}]);
	my ($head, $body) = $m->onload_render();
	like($head, qr/markercluster/i,               'cluster JS in head');
	like($body, qr/markerClusterGroup/,            'markerClusterGroup 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 to map' => sub {
	my $m = HTML::OSM->new();
	$m->add_marker([$C{LAT_LONDON}, $C{LON_LONDON}]);
	my ($head, $body) = $m->onload_render();
	unlike($head, qr/markercluster/, 'no cluster assets in head');
	unlike($body, qr/clusterGroup/,  'no clusterGroup in body');
	like($body,   qr/addTo\(map\)/,  'marker added directly to map');
};

# Popup labels with single quotes must be JS-escaped so they don't break the
# surrounding JS single-quoted string literal.
subtest 'onload_render: popup label with single quote is JS-escaped' => sub {
	my $m = HTML::OSM->new();
	$m->add_marker([$C{LAT_LONDON}, $C{LON_LONDON}], html => "O'Brien's Bar");
	my (undef, $body) = $m->onload_render();
	like($body,   qr/O\\'Brien/,           'single quote JS-escaped');
	unlike($body, qr/O'Brien(?!\\)/,       'unescaped quote absent from popup');
};

# GPX URLs with special characters must also pass through _js_string.
subtest 'onload_render: GPX URL with single quote is JS-escaped' => sub {
	my $m = HTML::OSM->new();
	$m->add_marker([$C{LAT_LONDON}, $C{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 single-quote JS-escaped');
};

subtest 'onload_render: non-marker render with center provided succeeds' => sub {
	my $m = HTML::OSM->new();
	$m->center([$C{LAT_LONDON}, $C{LON_LONDON}]);
	$m->add_geojson({ type => 'FeatureCollection', features => [] });
	my @r;
	lives_ok { @r = $m->onload_render() } 'center + GeoJSON-only lives';
	is(scalar @r, 2, 'two-element result');
	like($r[1], qr/L\.geoJSON/, 'GeoJSON in body');
};

# ── Global state integrity ────────────────────────────────────────────────────
# None of the public methods should clobber $@ or $_ — callers depend on these.

subtest 'onload_render: does not clobber $_ on success' => sub {
	my $m = HTML::OSM->new();
	$m->add_marker([$C{LAT_LONDON}, $C{LON_LONDON}]);
	local $_ = 'do_not_touch';
	$m->onload_render();
	is($_, 'do_not_touch', 'onload_render preserves $_');
};

subtest 'zoom: does not clobber $@ on success' => sub {
	my $m = HTML::OSM->new();
	eval { die 'sentinel' };
	my $before = $@;
	$m->zoom(5);
	is($@, $before, 'zoom() preserves $@');
};

subtest 'add_geojson: does not clobber $@ on success' => sub {
	my $m = HTML::OSM->new();
	eval { die 'sentinel' };
	my $before = $@;
	$m->add_geojson({ type => 'FeatureCollection', features => [] });
	is($@, $before, 'add_geojson preserves $@');
};

restore_all();
done_testing();



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