← Back to index

lib/BDD/Behave/Formatter/HTML.rakumod

109 / 118 lines covered (92.4%)

1use BDD::Behave::Failures;2use BDD::Behave::Formatter;34unit class BDD::Behave::Formatter::HTML does BDD::Behave::Formatter;56has @!fragments;7has Bool $!multi-file = False;8has Bool $!emitted    = False;9has Int  $!failure-watermark = 0;10has Str  $!pending-auto-description;11has Real $!started-at;1213method name(--> Str) { 'html' }1415method suite-loading(Str :$file) { }1617method suite-start($suite, Bool :$multi-file = False) {18  $!multi-file = $multi-file;19  $!started-at //= now.Real;20  if $multi-file {21    my $file = $suite.file.basename;22    @!fragments.push: '<h2 class="suite-file">' ~ self!escape($file) ~ '</h2>';23  }24}2526method suite-end($suite) { }2728method group-start($group) {29  @!fragments.push: '<details open class="group">'30                  ~ '<summary class="group-summary">'31                  ~ self!escape($group.description)32                  ~ '</summary>'33                  ~ '<div class="group-body">';34}3536method group-end($group) {37  @!fragments.push: '</div></details>';38}3940method group-around-skipped($group) {41  @!fragments.push: '<div class="example skipped">'42                  ~ '<span class="marker">⊘</span> '43                  ~ '<span class="desc">(group skipped: around-all did not invoke continuation)</span>'44                  ~ '</div>';45}4647method example-start($example, Bool :$auto = False) {48  $!failure-watermark = Failures.list.elems;49  $!pending-auto-description = Str;50}5152method example-auto-description($example, Str :$description) {53  $!pending-auto-description = $description;54}5556method !escape(Str $s --> Str) {57  return '' unless $s.defined;58  my $r = $s;59  $r = $r.subst('&', '&amp;', :g);60  $r = $r.subst('<', '&lt;',  :g);61  $r = $r.subst('>', '&gt;',  :g);62  $r = $r.subst('"', '&quot;',:g);63  $r;64}6566method !location($example --> Str) {67  '<span class="location">'68  ~ self!escape("{$example.file}:{$example.line}")69  ~ '</span>';70}7172method !duration-span($example --> Str) {73  return '' unless $example.duration.defined;74  '<span class="duration">'75  ~ sprintf('%.3fs', $example.duration)76  ~ '</span>';77}7879method !render-example($example, Str $status, Str $marker, Str :$detail) {80  my $description = $!pending-auto-description // $example.description;81  my $body = '<div class="example ' ~ $status ~ '">'82           ~ '<span class="marker">' ~ $marker ~ '</span> '83           ~ '<span class="desc">' ~ self!escape($description) ~ '</span> '84           ~ self!duration-span($example)85           ~ ' ' ~ self!location($example);86  $body ~= '<pre class="failure-detail">' ~ self!escape($detail) ~ '</pre>'87    if $detail.defined && $detail.chars;88  $body ~= '</div>';89  @!fragments.push: $body;90}9192method example-pass($example) {93  self!render-example($example, 'pass', '✓');94  $!pending-auto-description = Str;95}9697method example-fail($example, :$failure-info) {98  my @lines;99  if $failure-info.defined && $failure-info<exception>.defined {100    @lines.push: 'Exception: ' ~ $failure-info<exception>.message;101  }102  my $count = Failures.list.elems;103  if $count > $!failure-watermark {104    for Failures.list[$!failure-watermark .. $count - 1] -> $f {105      next if $f.from-runner-exception;106      @lines.push: "{$f.file}:{$f.line}";107      if $f.message.defined {108        @lines.push: "  $_" for $f.message.lines;109      } else {110        my $op = $f.negated ?? 'not to be' !! 'to be';111        @lines.push: "  Expected: {$f.given.gist}";112        @lines.push: "  $op:     {$f.expected.gist}";113      }114    }115  }116  self!render-example($example, 'fail', '✗', :detail(@lines.join("\n")));117  $!pending-auto-description = Str;118}119120method example-pending($example) {121  my $reason = $example.get-metadata('pending-reason') // 'pending';122  self!render-example($example, 'pending', '⏸',123                      :detail("pending: $reason"));124}125126method example-skipped($example) {127  self!render-example($example, 'skipped', '⊘');128}129130method example-around-skipped($example) {131  self!render-example($example, 'skipped', '⊘',132                      :detail('around-each did not invoke continuation'));133}134135method example-slow($example, Real :$threshold) { }136137method run-summary(138  $result,139  Bool :$aborted   = False,140  Int  :$fail-fast = 0,141  Str  :$order     = 'defined',142  Int  :$seed,143) {144  return if $!multi-file;145  self!emit($result, :$aborted, :$order, :$seed);146}147148method profile-summary(@records, Int :$limit)        { }149150method benchmark-summary-section(151  @summaries, @regressions,152  Real     :$threshold,153  Str      :$format,154  IO::Path :$output,155  :$runner,156) { }157158method multi-file-overall($result, Str :$order = 'defined', Int :$seed) {159  self!emit($result, :$order, :$seed);160}161162method multi-file-profile($runner, @records, Int :$limit)        { }163method multi-file-benchmark(164  $runner,165  @summaries, @regressions,166  Real     :$threshold,167  Str      :$format,168  IO::Path :$output,169) { }170171method load-errors(@errors) {172  for @errors -> $err {173    my $file    = ($err<file>    // '').Str;174    my $message = ($err<message> // '').Str;175    @!fragments.push: '<div class="load-error">'176                    ~ '<strong>Load error in ' ~ self!escape($file) ~ '</strong>'177                    ~ '<pre>' ~ self!escape($message) ~ '</pre>'178                    ~ '</div>';179  }180}181182method !summary-line($result --> Str) {183  my @parts;184  @parts.push: "{$result.total} example" ~ ($result.total == 1 ?? '' !! 's');185  @parts.push: "{$result.failed} failed"   if $result.failed  > 0;186  @parts.push: "{$result.pending} pending" if $result.pending > 0;187  @parts.push: "{$result.skipped} skipped" if $result.skipped > 0;188  @parts.push: "{$result.passed} passed"   if $result.passed  > 0;189  @parts.join(', ');190}191192method !style(--> Str) {193  q:to/CSS/;194body { font-family: -apple-system, sans-serif; margin: 1em; color: #222; }195h1 { margin-bottom: 0.2em; }196.summary { font-weight: bold; padding: 0.5em; background: #f0f0f0; border-radius: 4px; }197.summary.has-failures { background: #fee; }198.suite-file { margin-top: 1em; }199.group { margin-left: 1em; padding-left: 0.5em; border-left: 1px solid #eee; }200.group > .group-summary { font-weight: bold; cursor: pointer; padding: 0.2em 0; }201.group-body { margin-left: 1em; }202.example { padding: 0.15em 0.3em; margin: 0.15em 0; font-family: monospace; }203.example .marker { display: inline-block; width: 1.2em; text-align: center; font-weight: bold; }204.example.pass { color: #1b8a3a; }205.example.fail { color: #b30000; background: #fff5f5; }206.example.pending { color: #1370c4; background: #f5faff; }207.example.skipped { color: #777; }208.duration { color: #999; font-size: 0.85em; margin-left: 0.5em; }209.location { color: #aaa; font-size: 0.85em; margin-left: 0.5em; }210.failure-detail { background: #fff0f0; border-left: 3px solid #b30000; padding: 0.5em; margin: 0.3em 0 0.3em 1em; white-space: pre-wrap; }211.load-error { background: #fee; padding: 0.5em; margin: 0.5em 0; border-radius: 4px; }212CSS213}214215method !emit($result, Bool :$aborted = False, Str :$order = 'defined', Int :$seed) {216  return if $!emitted;217  $!emitted = True;218219  my $duration = $!started-at.defined ?? (now.Real - $!started-at) !! 0e0;220  my $summary = self!summary-line($result);221  my $summary-class = $result.failed > 0 ?? 'summary has-failures' !! 'summary';222223  say '<!DOCTYPE html>';224  say '<html lang="en">';225  say '<head>';226  say '<meta charset="utf-8">';227  say '<title>Behave Test Report</title>';228  say '<style>';229  print self!style;230  say '</style>';231  say '</head>';232  say '<body>';233  say '<h1>Behave Test Report</h1>';234  say '<p class="' ~ $summary-class ~ '">' ~ self!escape($summary)235                                            ~ ' (' ~ sprintf('%.3fs', $duration) ~ ')'236                                            ~ '</p>';237  if $order eq 'random' && $seed.defined {238    say '<p class="meta">Randomized with seed ' ~ $seed ~ '</p>';239  }240  if $aborted {241    say '<p class="meta aborted">Run was aborted.</p>';242  }243244  say '<div class="results">';245  for @!fragments -> $frag { say $frag }246  say '</div>';247  say '</body>';248  say '</html>';249}