class Minitest::Reporters::HtmlReporter

The default ERB template uses JQuery and Bootstrap, both of these are included by referencing the CDN sites
The report is generated using ERB. A custom ERB template can be provided but it is not required
On each test run all files in the reports directory are deleted, this prevents a build up of old reports
When using Minitest Specs, the number prefix is dropped from the name of the test so that it reads well
so that failing and skipped tests are at the top.
The reporter sorts the results alphabetically and then by results
a shared link
This is recommended to be used with a CI server, where the report is kept as an artifact and is accessible via
A reporter for generating HTML test reports

def compare_suites(suite_a, suite_b)

Tests suites which have skipped tests are given second highest priority
Test suites which have failing tests are given highest order
Test suites are first ordered by evaluating the results of the tests, then by test suite name
def compare_suites(suite_a, suite_b)
  return compare_suites_by_name(suite_a, suite_b) if suite_a[:has_errors_or_failures] && suite_b[:has_errors_or_failures]
  return -1 if suite_a[:has_errors_or_failures] && !suite_b[:has_errors_or_failures]
  return 1 if !suite_a[:has_errors_or_failures] && suite_b[:has_errors_or_failures]
  return compare_suites_by_name(suite_a, suite_b) if suite_a[:has_skipps] && suite_b[:has_skipps]
  return -1 if suite_a[:has_skipps] && !suite_b[:has_skipps]
  return 1 if !suite_a[:has_skipps] && suite_b[:has_skipps]
  compare_suites_by_name(suite_a, suite_b)
end

def compare_suites_by_name(suite_a, suite_b)

def compare_suites_by_name(suite_a, suite_b)
  suite_a[:name] <=> suite_b[:name]
end

def compare_tests(test_a, test_b)

Tests which are skipped are given second highest priority
Tess which fail are given highest order
Tests are first ordered by evaluating the results of the tests, then by tests names
def compare_tests(test_a, test_b)
  return compare_tests_by_name(test_a, test_b) if test_fail_or_error?(test_a) && test_fail_or_error?(test_b)
  return -1 if test_fail_or_error?(test_a) && !test_fail_or_error?(test_b)
  return 1 if !test_fail_or_error?(test_a) && test_fail_or_error?(test_b)
  return compare_tests_by_name(test_a, test_b) if test_a.skipped? && test_b.skipped?
  return -1 if test_a.skipped? && !test_b.skipped?
  return 1 if !test_a.skipped? && test_b.skipped?
  compare_tests_by_name(test_a, test_b)
end

def compare_tests_by_name(test_a, test_b)

def compare_tests_by_name(test_a, test_b)
  friendly_name(test_a) <=> friendly_name(test_b)
end

def friendly_name(test)

Trims off the number prefix on test names when using Minitest Specs
def friendly_name(test)
  groups = test.name.scan(/(test_\d+_)(.*)/i)
  return test.name if groups.empty?
  "it #{groups[0][1]}"
end

def html_file

def html_file
  "#{@reports_path}/#{@output_filename}"
end

def initialize(args = {})

:output_filename - the report's filename, defaults to 'index.html'
:mode - Useful for debugging, :terse suppresses errors and is the default, :verbose lets errors bubble up
:erb_template - the path to a custom ERB template, defaults to the supplied ERB template
:reports_dir - the directory the reports should be written to, defaults to 'test/html_reports'
:title - the title that will be used in the report, defaults to 'Test Results'
The constructor takes a hash, and uses the following keys:
def initialize(args = {})
  super({})
  defaults = {
    :title           => 'Test Results',
    :erb_template    => "#{File.dirname(__FILE__)}/../templates/index.html.erb",
    :reports_dir     => ENV['MINITEST_HTML_REPORTS_DIR'] || 'test/html_reports',
    :mode            => :safe,
    :output_filename => ENV['MINITEST_HTML_REPORTS_FILENAME'] || 'index.html',
  }
  settings = defaults.merge(args)
  @mode = settings[:mode]
  @title = settings[:title]
  @erb_template = settings[:erb_template]
  @output_filename = settings[:output_filename]
  reports_dir = settings[:reports_dir]
  @reports_path = File.absolute_path(reports_dir)
end

def location(exception)

taken from the JUnit reporter
def location(exception)
  last_before_assertion = ''
  exception.backtrace.reverse_each do |s|
    break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/
    last_before_assertion = s
  end
  last_before_assertion.sub(/:in .*$/, '')
end

def message_for(test)

based on message_for(test) from the JUnit reporter
def message_for(test)
  suite = test.class
  name = test.name
  e = test.failure
  if test.passed?
    nil
  elsif test.skipped?
    "Skipped:\n#{name}(#{suite}) [#{location(e)}]:\n#{e.message}\n"
  elsif test.failure
    "Failure:\n#{name}(#{suite}) [#{location(e)}]:\n#{e.message}\n"
  elsif test.error?
    "Error:\n#{name}(#{suite}):\n#{e.message}"
  end
end

def passes

The number of tests that passed
def passes
  count - failures - errors - skips
end

def percent_errors_failures

The percentage of tests that failed
def percent_errors_failures
  ((errors + failures) / count.to_f * 100).to_i
end

def percent_passes

The percentage of tests that passed, calculated in a way that avoids rounding errors
def percent_passes
  100 - percent_skipps - percent_errors_failures
end

def percent_skipps

The percentage of tests that were skipped
def percent_skipps
  (skips / count.to_f * 100).to_i
end

def report

Called by the framework to generate the report
def report
  super
  begin
    puts "Writing HTML reports to #{@reports_path}"
    erb_str = File.read(@erb_template)
    renderer = ERB.new(erb_str)
    tests_by_suites = tests.group_by { |test| test_class(test) } # taken from the JUnit reporter
    suites = tests_by_suites.map do |suite, tests|
      suite_summary = summarize_suite(suite, tests)
      suite_summary[:tests] = tests.sort { |a, b| compare_tests(a, b) }
      suite_summary
    end
    suites.sort! { |a, b| compare_suites(a, b) }
    result = renderer.result(binding)
    File.open(html_file, 'w') do |f|
      f.write(result)
    end
  # rubocop:disable Lint/RescueException
  rescue Exception => e
    puts 'There was an error writing the HTML report'
    puts 'This may have been caused by cancelling the test run'
    puts 'Use mode => :verbose in the HTML reporters constructor to see more detail' if @mode == :terse
    puts 'Use mode => :terse in the HTML reporters constructor to see less detail' if @mode != :terse
    raise e if @mode != :terse
  end
  # rubocop:enable Lint/RescueException
end

def start

def start
  super
  puts "Emptying #{@reports_path}"
  FileUtils.mkdir_p(@reports_path)
  File.delete(html_file) if File.exist?(html_file)
end

def summarize_suite(suite, tests)

based on analyze_suite from the JUnit reporter
def summarize_suite(suite, tests)
  summary = Hash.new(0)
  summary[:name] = suite.to_s
  tests.each do |test|
    summary[:"#{result(test)}_count"] += 1
    summary[:assertion_count] += test.assertions
    summary[:test_count] += 1
    summary[:time] += test.time
  end
  summary[:has_errors_or_failures] = (summary[:fail_count] + summary[:error_count]) > 0
  summary[:has_skipps] = summary[:skip_count] > 0
  summary
end

def test_fail_or_error?(test)

def test_fail_or_error?(test)
  test.error? || test.failure
end

def total_time_to_hms

def total_time_to_hms
  return ('%.2fs' % total_time) if total_time < 1
  hours = (total_time / (60 * 60)).round
  minutes = ((total_time / 60) % 60).round.to_s.rjust(2, '0')
  seconds = (total_time % 60).round.to_s.rjust(2, '0')
  "#{hours}h#{minutes}m#{seconds}s"
end