Back to Repositories

Testing Chart Generation and Security Features in Chartkick

This test suite validates the Chartkick library’s charting functionality, covering various chart types, options handling, and security features. The tests ensure proper rendering, configuration, and data handling for different chart visualizations.

Test Coverage Overview

The test suite provides comprehensive coverage of Chartkick’s core features:
  • Multiple chart type validations (line, pie, column, bar, area, scatter, geo)
  • Data handling and escaping for security
  • Chart configuration options and dimensions
  • Options merging and inheritance
  • DOM element generation and attributes

Implementation Analysis

The testing approach utilizes Minitest framework with a systematic pattern:
  • Setup method initializing test data and content storage
  • Individual test methods for each chart type
  • Security testing for XSS prevention
  • Validation of option handling and configuration inheritance
  • DOM element attribute verification

Technical Details

Testing infrastructure includes:
  • Minitest as the testing framework
  • Chartkick::Helper module integration
  • Custom assertions for chart validation
  • Mock content_for implementation
  • Chart JSON generation validation

Best Practices Demonstrated

The test suite exhibits several testing best practices:
  • Isolated test cases for each feature
  • Security testing for input validation
  • Edge case handling for dimensions and attributes
  • Clean test state management
  • Consistent assertion patterns

ankane/chartkick

test/chartkick_test.rb

            
require_relative "test_helper"

class ChartkickTest < Minitest::Test
  include Chartkick::Helper

  def setup
    @data = [[34, 42], [56, 49]]
    @content_for = {}
  end

  def test_line_chart
    assert_chart line_chart(@data)
  end

  def test_pie_chart
    assert_chart pie_chart(@data)
  end

  def test_column_chart
    assert_chart column_chart(@data)
  end

  def test_bar_chart
    assert_chart bar_chart(@data)
  end

  def test_area_chart
    assert_chart area_chart(@data)
  end

  def test_scatter_chart
    assert_chart scatter_chart(@data)
  end

  def test_geo_chart
    assert_chart geo_chart(@data)
  end

  def test_timeline
    assert_chart timeline(@data)
  end

  def test_escape_data
    bad_data = "</script><script>alert('xss')</script>"
    assert_match "\\u003cscript\\u003e", column_chart(bad_data)
    refute_match "<script>alert", column_chart(bad_data)
  end

  def test_escape_options
    bad_options = {xss: "</script><script>alert('xss')</script>"}
    assert_match "\\u003cscript\\u003e", column_chart([], **bad_options)
    refute_match "<script>alert", column_chart([], **bad_options)
  end

  def test_options_not_mutated
    options = {id: "boom"}
    line_chart @data, **options
    assert_equal "boom", options[:id]
  end

  def test_deep_merge_different_inner_key
    global_option = {library: {backgroundColor: "#eee"}}
    local_option = {library: {title: "test"}}
    correct_merge = {library: {backgroundColor: "#eee", title: "test"}}
    assert_equal Chartkick::Utils.deep_merge(global_option, local_option), correct_merge
  end

  def test_deep_merge_same_inner_key
    global_option = {library: {backgroundColor: "#eee"}}
    local_option = {library: {backgroundColor: "#fff"}}
    correct_merge = {library: {backgroundColor: "#fff"}}
    assert_equal Chartkick::Utils.deep_merge(global_option, local_option), correct_merge
  end

  def test_id
    assert_match "id=\"test-id\"", line_chart(@data, id: "test-id")
  end

  def test_id_escaped
    assert_match "id=\"test-123"\"", line_chart(@data, id: "test-123\"")
  end

  def test_height_pixels
    assert_match "height: 100px;", line_chart(@data, height: "100px")
  end

  def test_height_percent
    assert_match "height: 100%;", line_chart(@data, height: "100%")
  end

  def test_height_dot
    assert_match "height: 2.5rem;", line_chart(@data, height: "2.5rem")
  end

  def test_height_quote
    error = assert_raises(ArgumentError) do
      line_chart(@data, height: "150px\"")
    end
    assert_equal "Invalid height", error.message
  end

  def test_height_semicolon
    error = assert_raises(ArgumentError) do
      line_chart(@data, height: "150px;background:123")
    end
    assert_equal "Invalid height", error.message
  end

  def test_width_pixels
    assert_match "width: 100px;", line_chart(@data, width: "100px")
  end

  def test_width_percent
    assert_match "width: 100%;", line_chart(@data, width: "100%")
  end

  def test_width_dot
    assert_match "width: 2.5rem;", line_chart(@data, width: "2.5rem")
  end

  def test_width_quote
    error = assert_raises(ArgumentError) do
      line_chart(@data, width: "80%\"")
    end
    assert_equal "Invalid width", error.message
  end

  def test_width_semicolon
    error = assert_raises(ArgumentError) do
      line_chart(@data, width: "80%;background:123")
    end
    assert_equal "Invalid width", error.message
  end

  def test_loading
    assert_match ">Loading!!</div>", line_chart(@data, loading: "Loading!!")
  end

  def test_loading_escaped
    assert_match "<b>Loading!!</b>", line_chart(@data, loading: "<b>Loading!!</b>")
    refute_match "<b>", line_chart(@data, loading: "<b>Loading!!</b>")
  end

  def test_nonce
    assert_match "<script nonce=\"test-123\">", line_chart(@data, nonce: "test-123")
  end

  def test_nonce_escaped
    assert_match "<script nonce=\"test-123"\">", line_chart(@data, nonce: "test-123\"")
  end

  def test_defer
    assert_output(nil, /defer option is no longer needed/) do
      line_chart(@data, defer: true)
    end
  end

  def test_content_for
    refute_match "<script", line_chart(@data, content_for: :charts_js)
    assert_match "<script", @content_for[:charts_js]
  end

  def test_default_options
    Chartkick.options = {id: "test-123"}
    assert_match "id=\"test-123\"", line_chart(@data)
  ensure
    Chartkick.options = {}
  end

  def test_chart_ids
    @chartkick_chart_id = 0
    3.times do |i|
      assert_match "chart-#{i + 1}", line_chart(@data)
    end
  end

  def test_chart_json
    assert_equal "[1,2,3]", [1, 2, 3].chart_json
    assert_equal %![{"name":"s1","data":[["t1",1],["t2",2]]}]!, {["s1", "t1"] => 1, ["s1", "t2"] => 2}.chart_json
    assert_equal %![{"name":"s1","data":[["t1",1],["t2",2]]}]!, [{name: "s1", data: {"t1" => 1, "t2" => 2}}].chart_json
  end

  def assert_chart(chart)
    assert_match "new Chartkick", chart
  end

  def content_for(value)
    @content_for[value] = yield
  end
end