HTML-D3
view release on metacpan or search on metacpan
t/integration.t view on Meta::CPAN
my $chart = HTML::D3->new(width => 1_024, height => 768, title => 'Dims');
my @data = (['Jan', 10], ['Feb', 20]);
# Every full-page method must embed the correct SVG dimensions.
for my $pair (
['render_bar_chart', $chart->render_bar_chart(\@data)],
['render_line_chart', $chart->render_line_chart(\@data)],
['render_line_chart_with_tooltips',$chart->render_line_chart_with_tooltips(\@data)],
) {
my ($name, $html) = @$pair;
like($html, qr/svg id="$SVG_ID" width="1024"/, "$name: width in SVG");
like($html, qr/height="768"/, "$name: height in SVG");
}
# Snippet methods use the same object dimensions.
my $snip = $chart->render_line_chart_snippet(\@data)->{html};
my $zoom = $chart->render_zoomable_line_chart_snippet(\@data)->{html};
like($snip, qr/width="1024"/, 'snippet: width in SVG');
like($snip, qr/height="768"/, 'snippet: height in SVG');
like($zoom, qr/width="1024"/, 'zoomable: width in SVG');
like($zoom, qr/height="768"/, 'zoomable: height in SVG');
# Multi-series methods also propagate dimensions.
my $ms = $chart->render_multi_series_line_chart_with_tooltips(\@MULTI_DATA);
like($ms, qr/svg id="$SVG_ID" width="1024"/, 'multi-series: width in SVG');
like($ms, qr/height="768"/, 'multi-series: height in SVG');
};
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# 6. Full-page html-tidy validation
#
# Every method that returns a complete HTML5 document must pass html-tidy with
# no errors. This is the critical end-to-end gate: it exercises the DOCTYPE,
# head, meta tags, D3 script tag, and tooltip JS strings simultaneously.
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
subtest 'all full-page methods produce html-tidy-valid documents' => sub {
my $chart = HTML::D3->new(title => 'Tidy', width => 800, height => 600);
html_tidy_ok(
$chart->render_bar_chart(\@SIMPLE_DATA),
'render_bar_chart output is valid HTML5',
);
html_tidy_ok(
$chart->render_line_chart(\@SIMPLE_DATA),
'render_line_chart output is valid HTML5',
);
html_tidy_ok(
$chart->render_line_chart_with_tooltips(\@SIMPLE_DATA),
'render_line_chart_with_tooltips output is valid HTML5',
);
html_tidy_ok(
$chart->render_multi_series_line_chart_with_tooltips(\@MULTI_DATA),
'render_multi_series_line_chart_with_tooltips output is valid HTML5',
);
html_tidy_ok(
$chart->render_multi_series_line_chart_with_animated_tooltips(\@MULTI_DATA),
'render_multi_series_line_chart_with_animated_tooltips output is valid HTML5',
);
html_tidy_ok(
$chart->render_multi_series_line_chart_with_legends(\@MULTI_DATA),
'render_multi_series_line_chart_with_legends output is valid HTML5',
);
html_tidy_ok(
$chart->render_multi_series_line_chart_with_interactive_legends(\@MULTI_DATA),
'render_multi_series_line_chart_with_interactive_legends output is valid HTML5',
);
};
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# 7. Snippet isolation: fragment methods produce no page-shell
#
# Both snippet methods must not contain any DOCTYPE, <html>, <head>, or <body>
# regardless of the object settings. They must be safe to splice directly into
# a host layout without corrupting its structure.
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
subtest 'snippet methods produce page-shell-free fragments in all configurations' => sub {
# Test with non-default dimensions to rule out hardcoded fragments.
my $chart = HTML::D3->new(width => 1_024, height => 768, title => 'Frag');
for my $pair (
['render_line_chart_snippet',
$chart->render_line_chart_snippet(\@SIMPLE_DATA)->{html}],
['render_zoomable_line_chart_snippet',
$chart->render_zoomable_line_chart_snippet(\@SIMPLE_DATA)->{html}],
) {
my ($name, $html) = @$pair;
unlike($html, qr/<!DOCTYPE/i, "$name: no DOCTYPE");
unlike($html, qr/<html/i, "$name: no <html> element");
unlike($html, qr/<head/i, "$name: no <head> element");
unlike($html, qr/<body/i, "$name: no <body> element");
like($html, qr/$D3_CDN/, "$name does NOT link to CDN (caller's responsibility)")
if 0; # Snippets deliberately omit the CDN script tag
unlike($html, qr/\Q$D3_CDN\E/,
"$name: caller is responsible for loading D3 -- CDN tag absent from fragment");
}
};
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# 8. encode_json called exactly once per render invocation (spy)
#
# Every render method must serialise its data exactly once. Calling encode_json
# more than once per invocation would be redundant; calling it zero times would
# mean the data is hardcoded or ignored.
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
subtest 'encode_json called exactly once per render, with correct data shape' => sub {
my $chart = HTML::D3->new();
for my $pair (
['render_bar_chart', sub { $chart->render_bar_chart(\@SIMPLE_DATA) }],
['render_line_chart', sub { $chart->render_line_chart(\@SIMPLE_DATA) }],
['render_line_chart_with_tooltips',
sub { $chart->render_line_chart_with_tooltips(\@SIMPLE_DATA) }],
['render_line_chart_snippet',
sub { $chart->render_line_chart_snippet(\@SIMPLE_DATA) }],
['render_zoomable_line_chart_snippet',
sub { $chart->render_zoomable_line_chart_snippet(\@SIMPLE_DATA) }],
['render_multi_series_line_chart_with_tooltips',
sub { $chart->render_multi_series_line_chart_with_tooltips(\@MULTI_DATA) }],
) {
my ($name, $code) = @$pair;
my $sp = spy('HTML::D3::encode_json');
$code->();
my @calls = $sp->();
t/integration.t view on Meta::CPAN
is($@, '', 'JSON::PP encodes multi-series data without error');
like($ms_json, qr/Series A/, 'JSON::PP preserves series name');
# Extra tooltip data shape (hashref third element).
my $extra_json = eval {
JSON::PP->new->encode([
{ label => 'Jan', value => 100, extra => { Region => 'North' } },
{ label => 'Feb', value => 200 },
]);
};
is($@, '', 'JSON::PP encodes extra-tooltip data without error');
like($extra_json, qr/"extra"/, 'JSON::PP preserves extra key');
diag("JSON::PP version: $JSON::PP::VERSION") if $ENV{TEST_VERBOSE};
no Test::Without::Module qw(Cpanel::JSON::XS JSON::XS);
}
# (b) HTML::D3 with the active JSON backend produces structurally correct output.
my $chart = HTML::D3->new(title => 'Backend Test', width => 800, height => 600);
my $ref_bar = $chart->render_bar_chart(\@SIMPLE_DATA);
my $ref_snip = $chart->render_line_chart_snippet(\@SIMPLE_DATA)->{html};
like($ref_bar, qr/<!DOCTYPE html>/i, 'active-backend run: bar has DOCTYPE');
like($ref_bar, qr/"label":"January"/, 'active-backend run: bar has correct JSON');
like($ref_snip, qr/"label":"January"/, 'active-backend run: snippet has correct JSON');
unlike($ref_snip, qr/<!DOCTYPE/i, 'active-backend run: snippet has no DOCTYPE');
diag('active JSON backend: ' . ref(JSON::MaybeXS->new)) if $ENV{TEST_VERBOSE};
};
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# 10. Global variable integrity
#
# Render methods must not clobber $_ (commonly used by callers in loops),
# $@ (exception state checked after eval blocks), or $! (errno).
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
subtest 'render methods do not clobber $_, $@, or $!' => sub {
my $chart = HTML::D3->new();
# Preset sentinel values.
local $_ = 'sentinel';
local $@ = '';
# $! is tricky to set reliably; we just check it does not become truthy.
$chart->render_bar_chart(\@SIMPLE_DATA);
is($_, 'sentinel', '$_ is unchanged after render_bar_chart');
is($@, '', '$@ is unchanged after render_bar_chart');
$chart->render_line_chart_with_tooltips(\@SIMPLE_DATA);
is($_, 'sentinel', '$_ is unchanged after render_line_chart_with_tooltips');
is($@, '', '$@ is unchanged after render_line_chart_with_tooltips');
$chart->render_line_chart_snippet(\@SIMPLE_DATA);
is($_, 'sentinel', '$_ is unchanged after render_line_chart_snippet');
is($@, '', '$@ is unchanged after render_line_chart_snippet');
$chart->render_zoomable_line_chart_snippet(\@SIMPLE_DATA);
is($_, 'sentinel', '$_ is unchanged after render_zoomable_line_chart_snippet');
$chart->render_multi_series_line_chart_with_interactive_legends(\@MULTI_DATA);
is($_, 'sentinel', '$_ is unchanged after render_multi_series_*_with_interactive_legends');
is($@, '', '$@ is unchanged after render_multi_series_*_with_interactive_legends');
};
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# 11. Extra tooltip data round-trip (snippet vs zoomable)
#
# The optional third element [label, value, \%extra] must be serialised
# consistently by both render_line_chart_snippet and
# render_zoomable_line_chart_snippet.
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
subtest 'extra tooltip data serialised consistently across snippet methods' => sub {
my $chart = HTML::D3->new();
my $snip_html = $chart->render_line_chart_snippet(\@EXTRA_DATA)->{html};
my $zoom_html = $chart->render_zoomable_line_chart_snippet(\@EXTRA_DATA)->{html};
for my $pair (['snippet', $snip_html], ['zoom', $zoom_html]) {
my ($name, $html) = @$pair;
like($html, qr/"extra":\{/,
"$name: extra object present in D3 JSON binding");
like($html, qr/Region/,
"$name: extra key 'Region' appears in JSON");
like($html, qr/North/,
"$name: extra value 'North' appears in JSON");
like($html, qr/Object\.entries\(d\.extra\)/,
"$name: mouseover handler iterates d.extra");
# Exactly one data point carries extra; the second point (plain pair) must
# produce no extra key at all.
my @hits = ($html =~ m{"extra":\{}g);
is(scalar @hits, 1, "$name: exactly one data point carries the extra object");
}
# Both methods must produce the same Region/North pair -- verify textual
# consistency of the embedded JSON.
like($snip_html, qr/"Region":"North"/, 'snippet: Region key has correct value');
like($zoom_html, qr/"Region":"North"/, 'zoom: Region key has correct value');
};
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# 12. Title propagation throughout the full render pipeline
#
# The object title must appear in every full-page output's <title> tag AND in
# the visible <h1> heading; it must NOT appear in fragment output (no <title>
# or <h1> in a fragment).
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
subtest 'title propagates to page <title> and <h1> but not to fragments' => sub {
Readonly my $TITLE => 'My Dashboard Title';
my $chart = HTML::D3->new(title => $TITLE);
# Full-page methods.
for my $html (
$chart->render_bar_chart(\@SIMPLE_DATA),
$chart->render_line_chart(\@SIMPLE_DATA),
$chart->render_line_chart_with_tooltips(\@SIMPLE_DATA),
$chart->render_multi_series_line_chart_with_tooltips(\@MULTI_DATA),
$chart->render_multi_series_line_chart_with_legends(\@MULTI_DATA),
$chart->render_multi_series_line_chart_with_interactive_legends(\@MULTI_DATA),
) {
like($html, qr/<title>\Q$TITLE\E<\/title>/, 'title appears in <title> tag');
like($html, qr/\Q$TITLE\E<\/h1>/, 'title appears in visible <h1>');
}
# Fragment methods must NOT contain <title> or <h1>.
for my $html (
$chart->render_line_chart_snippet(\@SIMPLE_DATA)->{html},
$chart->render_zoomable_line_chart_snippet(\@SIMPLE_DATA)->{html},
) {
unlike($html, qr/<title>/i, 'fragment has no <title> element');
unlike($html, qr/<h1/i, 'fragment has no <h1> element');
}
};
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# 13. D3 CDN link: full-page methods load D3; fragment methods do not
#
# Fragment callers are responsible for loading D3 in their host page.
# Full-page methods must embed the CDN <script> tag in their <head>.
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
subtest 'D3 CDN script loaded by full-page methods, absent from fragments' => sub {
my $chart = HTML::D3->new();
for my $html (
$chart->render_bar_chart(\@SIMPLE_DATA),
$chart->render_line_chart(\@SIMPLE_DATA),
$chart->render_line_chart_with_tooltips(\@SIMPLE_DATA),
$chart->render_multi_series_line_chart_with_tooltips(\@MULTI_DATA),
) {
like($html, qr/\Q$D3_CDN\E/, "full-page output embeds D3 CDN script tag");
}
for my $html (
$chart->render_line_chart_snippet(\@SIMPLE_DATA)->{html},
$chart->render_zoomable_line_chart_snippet(\@SIMPLE_DATA)->{html},
) {
unlike($html, qr/\Q$D3_CDN\E/,
'fragment omits D3 CDN (caller is responsible)');
}
};
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# 14. html-tidy safety: </b> must never appear in any render output
#
# The POD formally specifies that all tooltip JS strings use <\/b> (with
# backslash) to satisfy html-tidy. This cross-method assertion runs the check
# across every method in one sweep.
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
subtest 'no raw </b> tag appears in any render output' => sub {
my $chart = HTML::D3->new();
for my $pair (
['render_bar_chart',
$chart->render_bar_chart(\@SIMPLE_DATA)],
['render_line_chart',
$chart->render_line_chart(\@SIMPLE_DATA)],
['render_line_chart_with_tooltips',
$chart->render_line_chart_with_tooltips(\@SIMPLE_DATA)],
['render_line_chart_snippet',
$chart->render_line_chart_snippet(\@SIMPLE_DATA)->{html}],
['render_zoomable_line_chart_snippet',
$chart->render_zoomable_line_chart_snippet(\@SIMPLE_DATA)->{html}],
['render_multi_series_line_chart_with_tooltips',
$chart->render_multi_series_line_chart_with_tooltips(\@MULTI_DATA)],
['render_multi_series_line_chart_with_animated_tooltips',
$chart->render_multi_series_line_chart_with_animated_tooltips(\@MULTI_DATA)],
['render_multi_series_line_chart_with_legends',
$chart->render_multi_series_line_chart_with_legends(\@MULTI_DATA)],
['render_multi_series_line_chart_with_interactive_legends',
$chart->render_multi_series_line_chart_with_interactive_legends(\@MULTI_DATA)],
) {
my ($name, $html) = @$pair;
unlike($html, qr{</b>}, "$name: no raw </b> in output");
}
};
done_testing();
( run in 1.592 second using v1.01-cache-2.11-cpan-364913b4093 )