#!/usr/bin/env ruby require 'minitest/autorun' 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 def deprecated_arg_fn(arg1, arg2, arg3 = nil) Sass::Script::Value::List.new([arg1, arg2, arg3 || Sass::Script::Value::Null.new], :space) end declare :deprecated_arg_fn, [:arg1, :arg2, :arg3], :deprecated => [:arg_1, :arg_2, :arg3] declare :deprecated_arg_fn, [:arg1, :arg2], :deprecated => [:arg_1, :arg_2] 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 < MiniTest::Test # 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_clamps_bounds assert_equal("#1f1f1f", evaluate("hsl(10, -114, 12)")) assert_equal("white", evaluate("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_clamps_bounds assert_equal("#1f1f1f", evaluate("hsla(10, -114, 12, 1)")) assert_equal("rgba(255, 255, 255, 0)", evaluate("hsla(10, 10, 256%, 0)")) assert_equal("rgba(28, 24, 23, 0)", evaluate("hsla(10, 10, 10, -0.1)")) assert_equal("#1c1817", evaluate("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_hsla_percent_warning 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) refute_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_checks_type assert_error_message("$map: 12 is not a map for `map-get'", "map-get(12, bar)") end def test_map_merge assert_equal("(foo: 1, bar: 2, baz: 3)", perform("map-merge((foo: 1, bar: 2), (baz: 3))").to_sass) assert_equal("(foo: 1, bar: 2)", perform("map-merge((), (foo: 1, bar: 2))").to_sass) assert_equal("(foo: 1, bar: 2)", perform("map-merge((foo: 1, bar: 2), ())").to_sass) end def test_map_merge_checks_type assert_error_message("$map1: 12 is not a map for `map-merge'", "map-merge(12, (foo: 1))") assert_error_message("$map2: 12 is not a map for `map-merge'", "map-merge((foo: 1), 12)") end def test_map_remove assert_equal("(foo: 1, baz: 3)", perform("map-remove((foo: 1, bar: 2, baz: 3), bar)").to_sass) assert_equal("(foo: 1, baz: 3)", perform("map-remove($map: (foo: 1, bar: 2, baz: 3), $key: bar)").to_sass) assert_equal("()", perform("map-remove((foo: 1, bar: 2, baz: 3), foo, bar, baz)").to_sass) assert_equal("()", perform("map-remove((), foo)").to_sass) assert_equal("()", perform("map-remove((), foo, bar)").to_sass) end def test_map_remove_checks_type assert_error_message("$map: 12 is not a map for `map-remove'", "map-remove(12, foo)") end def test_map_keys assert_equal("foo, bar", perform("map-keys((foo: 1, bar: 2))").to_sass) assert_equal("()", perform("map-keys(())").to_sass) end def test_map_keys_checks_type assert_error_message("$map: 12 is not a map for `map-keys'", "map-keys(12)") end def test_map_values assert_equal("1, 2", perform("map-values((foo: 1, bar: 2))").to_sass) assert_equal("1, 2, 2", perform("map-values((foo: 1, bar: 2, baz: 2))").to_sass) assert_equal("()", perform("map-values(())").to_sass) end def test_map_values_checks_type assert_error_message("$map: 12 is not a map for `map-values'", "map-values(12)") end def test_map_has_key assert_equal "true", evaluate("map-has-key((foo: 1, bar: 1), foo)") assert_equal "false", evaluate("map-has-key((foo: 1, bar: 1), baz)") assert_equal "false", evaluate("map-has-key((), foo)") end def test_map_has_key_checks_type assert_error_message("$map: 12 is not a map for `map-has-key'", "map-has-key(12, foo)") end def test_keywords # The actual functionality is tested in tests where real arglists are passed. assert_error_message("$args: 12 is not a variable argument list for `keywords'", "keywords(12)") assert_error_message( "$args: (1 2 3) is not a variable argument list for `keywords'", "keywords(1 2 3)") end def test_partial_list_of_pairs_doesnt_work_as_a_map assert_raises(Sass::SyntaxError) {evaluate("map-get((foo bar, baz bang, bip), 1)")} assert_raises(Sass::SyntaxError) {evaluate("map-get((foo bar, baz bang, bip bap bop), 1)")} assert_raises(Sass::SyntaxError) {evaluate("map-get((foo bar), 1)")} end def test_assert_unit ctx = Sass::Script::Functions::EvaluationContext.new(Sass::Environment.new(nil, {})) ctx.assert_unit Sass::Script::Value::Number.new(10, ["px"], []), "px" ctx.assert_unit Sass::Script::Value::Number.new(10, [], []), nil begin ctx.assert_unit Sass::Script::Value::Number.new(10, [], []), "px" fail rescue ArgumentError => 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 # Regression test for #1638. def test_random_with_float_integer_limit result = perform("random(1.0)") assert_kind_of Sass::Script::Number, result assert result.value >= 0, "Random number was below 0" assert result.value <= 1, "Random number was above 1" 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 refute_equal evaluate("random()"), evaluate("random()") end def test_deprecated_arg_names assert_warning < .bar\" to \".foo\" for `selector-append'", "selector-append('.foo', '> .bar')") assert_error_message("Can't append \"*.bar\" to \".foo\" for `selector-append'", "selector-append('.foo', '*.bar')") assert_error_message("Can't append \"ns|suffix\" to \".foo\" for `selector-append'", "selector-append('.foo', 'ns|suffix')") end def test_selector_extend assert_equal(".foo .x, .foo .a .bar, .a .foo .bar", evaluate("selector-extend('.foo .x', '.x', '.a .bar')")) assert_equal(".foo .x, .foo .bang, .x.bar, .bar.bang", evaluate("selector-extend('.foo .x, .x.bar', '.x', '.bang')")) assert_equal(".y .x, .foo .x, .y .foo, .foo .foo", evaluate("selector-extend('.y .x', '.x, .y', '.foo')")) assert_equal(".foo .x, .foo .bar, .foo .bang", evaluate("selector-extend('.foo .x', '.x', '.bar, .bang')")) assert_equal(".foo.x, .foo", evaluate("selector-extend('.foo.x', '.x', '.foo')")) end def test_selector_extend_checks_types assert_error_message("$selector: 12 is not a valid selector: it must be a string,\n" + "a list of strings, or a list of lists of strings for `selector-extend'", "selector-extend(12, '.foo', '.bar')") assert_error_message("$extendee: 12 is not a valid selector: it must be a string,\n" + "a list of strings, or a list of lists of strings for `selector-extend'", "selector-extend('.foo', 12, '.bar')") assert_error_message("$extender: 12 is not a valid selector: it must be a string,\n" + "a list of strings, or a list of lists of strings for `selector-extend'", "selector-extend('.foo', '.bar', 12)") end def test_selector_extend_errors assert_error_message("Can't extend .bar .baz: can't extend nested selectors for " + "`selector-extend'", "selector-extend('.foo', '.bar .baz', '.bang')") assert_error_message("Can't extend >: invalid selector for `selector-extend'", "selector-extend('.foo', '>', '.bang')") assert_error_message(".bang > can't extend: invalid selector for `selector-extend'", "selector-extend('.foo', '.bar', '.bang >')") end def test_selector_replace assert_equal(".bar", evaluate("selector-replace('.foo', '.foo', '.bar')")) assert_equal(".foo.baz", evaluate("selector-replace('.foo.bar', '.bar', '.baz')")) assert_equal(".a .foo.baz", evaluate("selector-replace('.foo.bar', '.bar', '.a .baz')")) assert_equal(".foo.bar", evaluate("selector-replace('.foo.bar', '.baz.bar', '.qux')")) assert_equal(".bar.qux", evaluate("selector-replace('.foo.bar.baz', '.foo.baz', '.qux')")) assert_equal(":not(.bar)", evaluate("selector-replace(':not(.foo)', '.foo', '.bar')")) assert_equal(".bar", evaluate("selector-replace(':not(.foo)', ':not(.foo)', '.bar')")) end def test_selector_replace_checks_types assert_error_message("$selector: 12 is not a valid selector: it must be a string,\n" + "a list of strings, or a list of lists of strings for `selector-replace'", "selector-replace(12, '.foo', '.bar')") assert_error_message("$original: 12 is not a valid selector: it must be a string,\n" + "a list of strings, or a list of lists of strings for `selector-replace'", "selector-replace('.foo', 12, '.bar')") assert_error_message("$replacement: 12 is not a valid selector: it must be a string,\n" + "a list of strings, or a list of lists of strings for `selector-replace'", "selector-replace('.foo', '.bar', 12)") end def test_selector_replace_errors assert_error_message("Can't extend .bar .baz: can't extend nested selectors for " + "`selector-replace'", "selector-replace('.foo', '.bar .baz', '.bang')") assert_error_message("Can't extend >: invalid selector for `selector-replace'", "selector-replace('.foo', '>', '.bang')") assert_error_message(".bang > can't extend: invalid selector for `selector-replace'", "selector-replace('.foo', '.bar', '.bang >')") end def test_selector_unify assert_equal(".foo", evaluate("selector-unify('.foo', '.foo')")) assert_equal(".foo.bar", evaluate("selector-unify('.foo', '.bar')")) assert_equal(".foo.bar.baz", evaluate("selector-unify('.foo.bar', '.bar.baz')")) assert_equal(".a .b .foo.bar, .b .a .foo.bar", evaluate("selector-unify('.a .foo', '.b .bar')")) assert_equal(".a .foo.bar", evaluate("selector-unify('.a .foo', '.a .bar')")) assert_equal("", evaluate("selector-unify('p', 'a')")) assert_equal("", evaluate("selector-unify('.foo >', '.bar')")) assert_equal("", evaluate("selector-unify('.foo', '.bar >')")) assert_equal(".foo.baz, .foo.bang, .bar.baz, .bar.bang", evaluate("selector-unify('.foo, .bar', '.baz, .bang')")) end def test_selector_unify_checks_types assert_error_message("$selector1: 12 is not a valid selector: it must be a string,\n" + "a list of strings, or a list of lists of strings for `selector-unify'", "selector-unify(12, '.foo')") assert_error_message("$selector2: 12 is not a valid selector: it must be a string,\n" + "a list of strings, or a list of lists of strings for `selector-unify'", "selector-unify('.foo', 12)") end def test_simple_selectors assert_equal('(.foo,)', evaluate("inspect(simple-selectors('.foo'))")) assert_equal('.foo, .bar', evaluate("inspect(simple-selectors('.foo.bar'))")) assert_equal('.foo, .bar, :pseudo("flip, flap")', evaluate("inspect(simple-selectors('.foo.bar:pseudo(\"flip, flap\")'))")) end def test_simple_selectors_checks_types assert_error_message("$selector: 12 is not a string for `simple-selectors'", "simple-selectors(12)") end def test_simple_selectors_errors assert_error_message("$selector: \".foo .bar\" is not a compound selector for `simple-selectors'", "simple-selectors('.foo .bar')") assert_error_message("$selector: \".foo,.bar\" is not a compound selector for `simple-selectors'", "simple-selectors('.foo,.bar')") assert_error_message("$selector: \".#\" is not a valid selector: Invalid CSS after \".\": " + "expected class name, was \"#\" for `simple-selectors'", "simple-selectors('.#')") end def test_is_superselector assert_equal("true", evaluate("is-superselector('.foo', '.foo.bar')")) assert_equal("false", evaluate("is-superselector('.foo.bar', '.foo')")) assert_equal("true", evaluate("is-superselector('.foo', '.foo')")) assert_equal("true", evaluate("is-superselector('.bar', '.foo .bar')")) assert_equal("false", evaluate("is-superselector('.foo .bar', '.bar')")) assert_equal("true", evaluate("is-superselector('.foo .bar', '.foo > .bar')")) assert_equal("false", evaluate("is-superselector('.foo > .bar', '.foo .bar')")) end def test_is_superselector_checks_types assert_error_message("$super: 12 is not a valid selector: it must be a string,\n" + "a list of strings, or a list of lists of strings for `is-superselector'", "is-superselector(12, '.foo')") assert_error_message("$sub: 12 is not a valid selector: it must be a string,\n" + "a list of strings, or a list of lists of strings for `is-superselector'", "is-superselector('.foo', 12)") 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