#!/usr/bin/env ruby require 'test/unit' require File.dirname(__FILE__) + '/../test_helper' require File.dirname(__FILE__) + '/test_helper' require 'sass/script' require 'mock_importer' module Sass::Script::Functions def no_kw_args Sass::Script::Value::String.new("no-kw-args") end def only_var_args(*args) Sass::Script::Value::String.new("only-var-args("+args.map{|a| a.plus(Sass::Script::Value::Number.new(1)).to_s }.join(", ")+")") end declare :only_var_args, [], :var_args => true def only_kw_args(kwargs) Sass::Script::Value::String.new("only-kw-args(" + kwargs.keys.map {|a| a.to_s}.sort.join(", ") + ")") end declare :only_kw_args, [], :var_kwargs => true end module Sass::Script::Functions::UserFunctions def call_options_on_new_value str = Sass::Script::Value::String.new("foo") str.options[:foo] str end def user_defined Sass::Script::Value::String.new("I'm a user-defined string!") end def _preceding_underscore Sass::Script::Value::String.new("I'm another user-defined string!") end def fetch_the_variable environment.var('variable') end end module Sass::Script::Functions include Sass::Script::Functions::UserFunctions end class SassFunctionTest < Test::Unit::TestCase # Tests taken from: # http://www.w3.org/Style/CSS/Test/CSS3/Color/20070927/html4/t040204-hsl-h-rotating-b.htm # http://www.w3.org/Style/CSS/Test/CSS3/Color/20070927/html4/t040204-hsl-values-b.htm File.read(File.dirname(__FILE__) + "/data/hsl-rgb.txt").split("\n\n").each do |chunk| hsls, rgbs = chunk.strip.split("====") hsls.strip.split("\n").zip(rgbs.strip.split("\n")) do |hsl, rgb| hsl_method = "test_hsl: #{hsl} = #{rgb}" unless method_defined?(hsl_method) define_method(hsl_method) do assert_equal(evaluate(rgb), evaluate(hsl)) end end rgb_to_hsl_method = "test_rgb_to_hsl: #{rgb} = #{hsl}" unless method_defined?(rgb_to_hsl_method) define_method(rgb_to_hsl_method) do rgb_color = perform(rgb) hsl_color = perform(hsl) white = hsl_color.lightness == 100 black = hsl_color.lightness == 0 grayscale = white || black || hsl_color.saturation == 0 assert_in_delta(hsl_color.hue, rgb_color.hue, 0.0001, "Hues should be equal") unless grayscale assert_in_delta(hsl_color.saturation, rgb_color.saturation, 0.0001, "Saturations should be equal") unless white || black assert_in_delta(hsl_color.lightness, rgb_color.lightness, 0.0001, "Lightnesses should be equal") end end end end def test_hsl_kwargs assert_equal "#33cccc", evaluate("hsl($hue: 180, $saturation: 60%, $lightness: 50%)") end def test_hsl_checks_bounds assert_error_message("Saturation -114 must be between 0% and 100% for `hsl'", "hsl(10, -114, 12)"); assert_error_message("Lightness 256% must be between 0% and 100% for `hsl'", "hsl(10, 10, 256%)"); end def test_hsl_checks_types assert_error_message("$hue: \"foo\" is not a number for `hsl'", "hsl(\"foo\", 10, 12)"); assert_error_message("$saturation: \"foo\" is not a number for `hsl'", "hsl(10, \"foo\", 12)"); assert_error_message("$lightness: \"foo\" is not a number for `hsl'", "hsl(10, 10, \"foo\")"); end def test_hsla assert_equal "rgba(51, 204, 204, 0.4)", evaluate("hsla(180, 60%, 50%, 0.4)") assert_equal "#33cccc", evaluate("hsla(180, 60%, 50%, 1)") assert_equal "rgba(51, 204, 204, 0)", evaluate("hsla(180, 60%, 50%, 0)") assert_equal "rgba(51, 204, 204, 0.4)", evaluate("hsla($hue: 180, $saturation: 60%, $lightness: 50%, $alpha: 0.4)") end def test_hsla_checks_bounds assert_error_message("Saturation -114 must be between 0% and 100% for `hsla'", "hsla(10, -114, 12, 1)"); assert_error_message("Lightness 256% must be between 0% and 100% for `hsla'", "hsla(10, 10, 256%, 0)"); assert_error_message("Alpha channel -0.1 must be between 0 and 1 for `hsla'", "hsla(10, 10, 10, -0.1)"); assert_error_message("Alpha channel 1.1 must be between 0 and 1 for `hsla'", "hsla(10, 10, 10, 1.1)"); end def test_hsla_checks_types assert_error_message("$hue: \"foo\" is not a number for `hsla'", "hsla(\"foo\", 10, 12, 0.3)"); assert_error_message("$saturation: \"foo\" is not a number for `hsla'", "hsla(10, \"foo\", 12, 0)"); assert_error_message("$lightness: \"foo\" is not a number for `hsla'", "hsla(10, 10, \"foo\", 1)"); assert_error_message("$alpha: \"foo\" is not a number for `hsla'", "hsla(10, 10, 10, \"foo\")"); end def test_percentage assert_equal("50%", evaluate("percentage(.5)")) assert_equal("100%", evaluate("percentage(1)")) assert_equal("25%", evaluate("percentage(25px / 100px)")) assert_equal("50%", evaluate("percentage($number: 0.5)")) end def test_percentage_deprecated_arg_name assert_warning(< Sass::Script::Value::String.new('The variable')) assert_equal("The variable", evaluate("fetch_the_variable()", environment)) end def test_options_on_new_values_fails assert_error_message(< e assert_equal("Function rgba doesn't have an argument named $extra", e.message) end def test_keyword_args_must_have_signature evaluate("no-kw-args($fake: value)") flunk("Expected exception") rescue Sass::SyntaxError => e assert_equal("Function no_kw_args doesn't support keyword arguments", e.message) end def test_keyword_args_with_missing_argument evaluate("rgb($red: 255, $green: 255)") flunk("Expected exception") rescue Sass::SyntaxError => e assert_equal("Function rgb requires an argument named $blue", e.message) end def test_keyword_args_with_extra_argument evaluate("rgb($red: 255, $green: 255, $blue: 255, $purple: 255)") flunk("Expected exception") rescue Sass::SyntaxError => e assert_equal("Function rgb doesn't have an argument named $purple", e.message) end def test_keyword_args_with_positional_and_keyword_argument evaluate("rgb(255, 255, 255, $red: 255)") flunk("Expected exception") rescue Sass::SyntaxError => e assert_equal("Function rgb was passed argument $red both by position and by name", e.message) end def test_keyword_args_with_keyword_before_positional_argument evaluate("rgb($red: 255, 255, 255)") flunk("Expected exception") rescue Sass::SyntaxError => e assert_equal("Positional arguments must come before keyword arguments.", e.message) end def test_only_var_args assert_equal "only-var-args(2px, 3px, 4px)", evaluate("only-var-args(1px, 2px, 3px)") end def test_only_kw_args assert_equal "only-kw-args(a, b, c)", evaluate("only-kw-args($a: 1, $b: 2, $c: 3)") end def test_unique_id last_id, current_id = nil, evaluate("unique-id()") 50.times do last_id, current_id = current_id, evaluate("unique-id()") assert_match(/u[a-z0-9]{8}/, current_id) assert_not_equal last_id, current_id end end def test_map_get assert_equal "1", evaluate("map-get((foo: 1, bar: 2), foo)") assert_equal "2", evaluate("map-get((foo: 1, bar: 2), bar)") assert_equal "null", perform("map-get((foo: 1, bar: 2), baz)").to_sass assert_equal "null", perform("map-get((), foo)").to_sass end def test_map_get_deprecation_warning assert_warning(< e assert_equal "Expected 10 to have a unit of px", e.message end begin ctx.assert_unit Sass::Script::Value::Number.new(10, ["px"], []), nil fail rescue ArgumentError => e assert_equal "Expected 10px to be unitless", e.message end begin ctx.assert_unit Sass::Script::Value::Number.new(10, [], []), "px", "arg" fail rescue ArgumentError => e assert_equal "Expected $arg to have a unit of px but got 10", e.message end begin ctx.assert_unit Sass::Script::Value::Number.new(10, ["px"], []), nil, "arg" fail rescue ArgumentError => e assert_equal "Expected $arg to be unitless but got 10px", e.message end end def test_call_with_positional_arguments assert_equal evaluate("lighten(blue, 5%)"), evaluate("call(lighten, blue, 5%)") end def test_call_with_keyword_arguments assert_equal( evaluate("lighten($color: blue, $amount: 5%)"), evaluate("call(lighten, $color: blue, $amount: 5%)")) end def test_call_with_keyword_and_positional_arguments assert_equal( evaluate("lighten(blue, $amount: 5%)"), evaluate("call(lighten, blue, $amount: 5%)")) end def test_call_with_dynamic_name assert_equal( evaluate("lighten($color: blue, $amount: 5%)"), evaluate("call($fn, $color: blue, $amount: 5%)", env("fn" => Sass::Script::String.new("lighten")))) end def test_call_uses_local_scope assert_equal <= 0, "Random number was below 0" assert result.value <= 1, "Random number was above 1" end def test_random_with_limit_one # Passing 1 as the limit should always return 1, since limit calls return # integers from 1 to the argument, so when the argument is 1, its a predicatble # outcome assert "1", evaluate("random(1)") end def test_random_with_limit_too_low assert_error_message("$limit 0 must be greater than or equal to 1 for `random'", "random(0)") end def test_random_with_non_integer_limit assert_error_message("Expected $limit to be an integer but got 1.5 for `random'", "random(1.5)") end # This could *possibly* fail, but exceedingly unlikely def test_random_is_semi_unique if Sass::Script::Functions.instance_variable_defined?("@random_number_generator") Sass::Script::Functions.send(:remove_instance_variable, "@random_number_generator") end assert_not_equal evaluate("random()"), evaluate("random()") end ## Regression Tests def test_inspect_nested_empty_lists assert_equal "() ()", evaluate("inspect(() ())") end def test_saturation_bounds assert_equal "#fbfdff", evaluate("hsl(hue(#fbfdff), saturation(#fbfdff), lightness(#fbfdff))") end private def env(hash = {}, parent = nil) env = Sass::Environment.new(parent) hash.each {|k, v| env.set_var(k, v)} env end def evaluate(value, environment = env) result = perform(value, environment) assert_kind_of Sass::Script::Value::Base, result return result.to_s end def perform(value, environment = env) Sass::Script::Parser.parse(value, 0, 0).perform(environment) end def render(sass, options = {}) options[:syntax] ||= :scss munge_filename options options[:importer] ||= MockImporter.new Sass::Engine.new(sass, options).render end def assert_error_message(message, value) evaluate(value) flunk("Error message expected but not raised: #{message}") rescue Sass::SyntaxError => e assert_equal(message, e.message) end end