HTML-OSM
view release on metacpan or search on metacpan
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 {
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 {
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 )