mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-15 17:00:39 -07:00
Instead of having Condition parse the left op right, make BinaryExpression do it. Make sure all the tests pass as they used to in the process.
380 lines
14 KiB
Ruby
380 lines
14 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'test_helper'
|
|
|
|
class ConditionUnitTest < Minitest::Test
|
|
include Liquid
|
|
|
|
def setup
|
|
@context = Liquid::Context.new
|
|
end
|
|
|
|
def test_basic_condition
|
|
assert_equal(false, Condition.new(1, '==', 2).evaluate(Context.new))
|
|
assert_equal(true, Condition.new(1, '==', 1).evaluate(Context.new))
|
|
end
|
|
|
|
def test_default_operators_evalute_true
|
|
assert_evaluates_true(1, '==', 1)
|
|
assert_evaluates_true(1, '!=', 2)
|
|
assert_evaluates_true(1, '<>', 2)
|
|
assert_evaluates_true(1, '<', 2)
|
|
assert_evaluates_true(2, '>', 1)
|
|
assert_evaluates_true(1, '>=', 1)
|
|
assert_evaluates_true(2, '>=', 1)
|
|
assert_evaluates_true(1, '<=', 2)
|
|
assert_evaluates_true(1, '<=', 1)
|
|
# negative numbers
|
|
assert_evaluates_true(1, '>', -1)
|
|
assert_evaluates_true(-1, '<', 1)
|
|
assert_evaluates_true(1.0, '>', -1.0)
|
|
assert_evaluates_true(-1.0, '<', 1.0)
|
|
end
|
|
|
|
def test_default_operators_evalute_false
|
|
assert_evaluates_false(1, '==', 2)
|
|
assert_evaluates_false(1, '!=', 1)
|
|
assert_evaluates_false(1, '<>', 1)
|
|
assert_evaluates_false(1, '<', 0)
|
|
assert_evaluates_false(2, '>', 4)
|
|
assert_evaluates_false(1, '>=', 3)
|
|
assert_evaluates_false(2, '>=', 4)
|
|
assert_evaluates_false(1, '<=', 0)
|
|
assert_evaluates_false(1, '<=', 0)
|
|
end
|
|
|
|
def test_contains_works_on_strings
|
|
assert_evaluates_true('bob', 'contains', 'o')
|
|
assert_evaluates_true('bob', 'contains', 'b')
|
|
assert_evaluates_true('bob', 'contains', 'bo')
|
|
assert_evaluates_true('bob', 'contains', 'ob')
|
|
assert_evaluates_true('bob', 'contains', 'bob')
|
|
|
|
assert_evaluates_false('bob', 'contains', 'bob2')
|
|
assert_evaluates_false('bob', 'contains', 'a')
|
|
assert_evaluates_false('bob', 'contains', '---')
|
|
end
|
|
|
|
def test_contains_binary_encoding_compatibility_with_utf8
|
|
assert_evaluates_true('🙈'.b, 'contains', '🙈')
|
|
assert_evaluates_true('🙈', 'contains', '🙈'.b)
|
|
end
|
|
|
|
def test_invalid_comparation_operator
|
|
assert_evaluates_argument_error(1, '~~', 0)
|
|
end
|
|
|
|
def test_comparation_of_int_and_str
|
|
assert_evaluates_argument_error('1', '>', 0)
|
|
assert_evaluates_argument_error('1', '<', 0)
|
|
assert_evaluates_argument_error('1', '>=', 0)
|
|
assert_evaluates_argument_error('1', '<=', 0)
|
|
end
|
|
|
|
def test_hash_compare_backwards_compatibility
|
|
assert_nil(Condition.new({}, '>', 2).evaluate(Context.new))
|
|
assert_nil(Condition.new(2, '>', {}).evaluate(Context.new))
|
|
assert_equal(false, Condition.new({}, '==', 2).evaluate(Context.new))
|
|
assert_equal(true, Condition.new({ 'a' => 1 }, '==', 'a' => 1).evaluate(Context.new))
|
|
assert_equal(true, Condition.new({ 'a' => 2 }, 'contains', 'a').evaluate(Context.new))
|
|
end
|
|
|
|
def test_contains_works_on_arrays
|
|
@context = Liquid::Context.new
|
|
@context['array'] = [1, 2, 3, 4, 5]
|
|
array_expr = VariableLookup.parse("array")
|
|
|
|
assert_evaluates_false(array_expr, 'contains', 0)
|
|
assert_evaluates_true(array_expr, 'contains', 1)
|
|
assert_evaluates_true(array_expr, 'contains', 2)
|
|
assert_evaluates_true(array_expr, 'contains', 3)
|
|
assert_evaluates_true(array_expr, 'contains', 4)
|
|
assert_evaluates_true(array_expr, 'contains', 5)
|
|
assert_evaluates_false(array_expr, 'contains', 6)
|
|
assert_evaluates_false(array_expr, 'contains', "1")
|
|
end
|
|
|
|
def test_contains_returns_false_for_nil_operands
|
|
@context = Liquid::Context.new
|
|
assert_evaluates_false(VariableLookup.parse('not_assigned'), 'contains', '0')
|
|
assert_evaluates_false(0, 'contains', VariableLookup.parse('not_assigned'))
|
|
end
|
|
|
|
def test_contains_return_false_on_wrong_data_type
|
|
assert_evaluates_false(1, 'contains', 0)
|
|
end
|
|
|
|
def test_contains_with_string_left_operand_coerces_right_operand_to_string
|
|
assert_evaluates_true(' 1 ', 'contains', 1)
|
|
assert_evaluates_false(' 1 ', 'contains', 2)
|
|
end
|
|
|
|
def test_or_condition
|
|
condition = Condition.new(1, '==', 2)
|
|
assert_equal(false, condition.evaluate(Context.new))
|
|
|
|
condition.or(Condition.new(2, '==', 1))
|
|
|
|
assert_equal(false, condition.evaluate(Context.new))
|
|
|
|
condition.or(Condition.new(1, '==', 1))
|
|
|
|
assert_equal(true, condition.evaluate(Context.new))
|
|
end
|
|
|
|
def test_and_condition
|
|
condition = Condition.new(1, '==', 1)
|
|
|
|
assert_equal(true, condition.evaluate(Context.new))
|
|
|
|
condition.and(Condition.new(2, '==', 2))
|
|
|
|
assert_equal(true, condition.evaluate(Context.new))
|
|
|
|
condition.and(Condition.new(2, '==', 1))
|
|
|
|
assert_equal(false, condition.evaluate(Context.new))
|
|
end
|
|
|
|
def test_should_allow_custom_proc_operator
|
|
Condition.operators['starts_with'] = proc { |_cond, left, right| left =~ /^#{right}/ }
|
|
|
|
assert_evaluates_true('bob', 'starts_with', 'b')
|
|
assert_evaluates_false('bob', 'starts_with', 'o')
|
|
ensure
|
|
Condition.operators.delete('starts_with')
|
|
end
|
|
|
|
def test_left_or_right_may_contain_operators
|
|
@context = Liquid::Context.new
|
|
@context['one'] = @context['another'] = "gnomeslab-and-or-liquid"
|
|
|
|
assert_evaluates_true(VariableLookup.parse("one"), '==', VariableLookup.parse("another"))
|
|
end
|
|
|
|
def test_default_context_is_deprecated
|
|
if Gem::Version.new(Liquid::VERSION) >= Gem::Version.new('6.0.0')
|
|
flunk("Condition#evaluate without a context argument is to be removed")
|
|
end
|
|
|
|
_out, err = capture_io do
|
|
assert_equal(true, Condition.new(1, '==', 1).evaluate)
|
|
end
|
|
|
|
expected = "DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated " \
|
|
"and will be removed from Liquid 6.0.0."
|
|
assert_includes(err.lines.map(&:strip), expected)
|
|
end
|
|
|
|
def test_parse_expression
|
|
environment = Environment.build
|
|
parse_context = ParseContext.new(environment: environment)
|
|
parser = parse_context.new_parser('product.title')
|
|
result = parser.expression
|
|
|
|
assert_instance_of(VariableLookup, result)
|
|
assert_equal('product', result.name)
|
|
assert_equal(['title'], result.lookups)
|
|
end
|
|
|
|
def test_parser_expression_returns_method_literal_for_blank_and_empty
|
|
environment = Environment.build
|
|
parse_context = ParseContext.new(environment: environment)
|
|
parser = parse_context.new_parser('blank')
|
|
result = parser.expression
|
|
|
|
assert_instance_of(MethodLiteral, result)
|
|
end
|
|
|
|
# Tests for blank? comparison without ActiveSupport
|
|
#
|
|
# Ruby's standard library does not include blank? on String, Array, Hash, etc.
|
|
# ActiveSupport adds blank? but Liquid must work without it. These tests verify
|
|
# that Liquid implements blank? semantics internally for use in templates like:
|
|
# {% if x == blank %}...{% endif %}
|
|
#
|
|
# The blank? semantics match ActiveSupport's behavior:
|
|
# - nil and false are blank
|
|
# - Strings are blank if empty or contain only whitespace
|
|
# - Arrays and Hashes are blank if empty
|
|
# - true and numbers are never blank
|
|
|
|
def test_blank_with_whitespace_string
|
|
# Template authors expect " " to be blank since it has no visible content.
|
|
# This matches ActiveSupport's String#blank? which returns true for whitespace-only strings.
|
|
@context['whitespace'] = ' '
|
|
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
|
|
|
assert_evaluates_true(VariableLookup.parse('whitespace'), '==', blank_literal)
|
|
end
|
|
|
|
def test_blank_with_empty_string
|
|
# An empty string has no content, so it should be considered blank.
|
|
# This is the most basic case of a blank string.
|
|
@context['empty_string'] = ''
|
|
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
|
|
|
assert_evaluates_true(VariableLookup.parse('empty_string'), '==', blank_literal)
|
|
end
|
|
|
|
def test_blank_with_empty_array
|
|
# Empty arrays have no elements, so they are blank.
|
|
# Useful for checking if a collection has items: {% if products == blank %}
|
|
@context['empty_array'] = []
|
|
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
|
|
|
assert_evaluates_true(VariableLookup.parse('empty_array'), '==', blank_literal)
|
|
end
|
|
|
|
def test_blank_with_empty_hash
|
|
# Empty hashes have no key-value pairs, so they are blank.
|
|
# Useful for checking if settings/options exist: {% if settings == blank %}
|
|
@context['empty_hash'] = {}
|
|
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
|
|
|
assert_evaluates_true(VariableLookup.parse('empty_hash'), '==', blank_literal)
|
|
end
|
|
|
|
def test_blank_with_nil
|
|
# nil represents "nothing" and is the canonical blank value.
|
|
# Unassigned variables resolve to nil, so this enables: {% if missing_var == blank %}
|
|
@context['nil_value'] = nil
|
|
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
|
|
|
assert_evaluates_true(VariableLookup.parse('nil_value'), '==', blank_literal)
|
|
end
|
|
|
|
def test_blank_with_false
|
|
# false is considered blank to match ActiveSupport semantics.
|
|
# This allows {% if some_flag == blank %} to work when flag is false.
|
|
@context['false_value'] = false
|
|
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
|
|
|
assert_evaluates_true(VariableLookup.parse('false_value'), '==', blank_literal)
|
|
end
|
|
|
|
def test_not_blank_with_true
|
|
# true is a definite value, not blank.
|
|
# Ensures {% if flag == blank %} works correctly for boolean flags.
|
|
@context['true_value'] = true
|
|
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
|
|
|
assert_evaluates_false(VariableLookup.parse('true_value'), '==', blank_literal)
|
|
end
|
|
|
|
def test_not_blank_with_number
|
|
# Numbers (including zero) are never blank - they represent actual values.
|
|
# 0 is a valid quantity, not the absence of a value.
|
|
@context['number'] = 42
|
|
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
|
|
|
assert_evaluates_false(VariableLookup.parse('number'), '==', blank_literal)
|
|
end
|
|
|
|
def test_not_blank_with_string_content
|
|
# A string with actual content is not blank.
|
|
# This is the expected behavior for most template string comparisons.
|
|
@context['string'] = 'hello'
|
|
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
|
|
|
assert_evaluates_false(VariableLookup.parse('string'), '==', blank_literal)
|
|
end
|
|
|
|
def test_not_blank_with_non_empty_array
|
|
# An array with elements has content, so it's not blank.
|
|
# Enables patterns like {% unless products == blank %}Show products{% endunless %}
|
|
@context['array'] = [1, 2, 3]
|
|
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
|
|
|
assert_evaluates_false(VariableLookup.parse('array'), '==', blank_literal)
|
|
end
|
|
|
|
def test_not_blank_with_non_empty_hash
|
|
# A hash with key-value pairs has content, so it's not blank.
|
|
# Useful for checking if configuration exists: {% if config != blank %}
|
|
@context['hash'] = { 'a' => 1 }
|
|
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
|
|
|
assert_evaluates_false(VariableLookup.parse('hash'), '==', blank_literal)
|
|
end
|
|
|
|
# Tests for empty? comparison without ActiveSupport
|
|
#
|
|
# empty? is distinct from blank? - it only checks if a collection has zero elements.
|
|
# For strings, empty? checks length == 0, NOT whitespace content.
|
|
# Ruby's standard library has empty? on String, Array, and Hash, but Liquid
|
|
# provides a fallback implementation for consistency.
|
|
|
|
def test_empty_with_empty_string
|
|
# An empty string ("") has length 0, so it's empty.
|
|
# Different from blank - empty is a stricter check.
|
|
@context['empty_string'] = ''
|
|
empty_literal = Condition.class_variable_get(:@@method_literals)['empty']
|
|
|
|
assert_evaluates_true(VariableLookup.parse('empty_string'), '==', empty_literal)
|
|
end
|
|
|
|
def test_empty_with_whitespace_string_not_empty
|
|
# Whitespace strings have length > 0, so they are NOT empty.
|
|
# This is the key difference between empty and blank:
|
|
# " ".empty? => false, but " ".blank? => true
|
|
@context['whitespace'] = ' '
|
|
empty_literal = Condition.class_variable_get(:@@method_literals)['empty']
|
|
|
|
assert_evaluates_false(VariableLookup.parse('whitespace'), '==', empty_literal)
|
|
end
|
|
|
|
def test_empty_with_empty_array
|
|
# An array with no elements is empty.
|
|
# [].empty? => true
|
|
@context['empty_array'] = []
|
|
empty_literal = Condition.class_variable_get(:@@method_literals)['empty']
|
|
|
|
assert_evaluates_true(VariableLookup.parse('empty_array'), '==', empty_literal)
|
|
end
|
|
|
|
def test_empty_with_empty_hash
|
|
# A hash with no key-value pairs is empty.
|
|
# {}.empty? => true
|
|
@context['empty_hash'] = {}
|
|
empty_literal = Condition.class_variable_get(:@@method_literals)['empty']
|
|
|
|
assert_evaluates_true(VariableLookup.parse('empty_hash'), '==', empty_literal)
|
|
end
|
|
|
|
def test_nil_is_not_empty
|
|
# nil is NOT empty - empty? checks if a collection has zero elements.
|
|
# nil is not a collection, so it cannot be empty.
|
|
# This differs from blank: nil IS blank, but nil is NOT empty.
|
|
@context['nil_value'] = nil
|
|
empty_literal = Condition.class_variable_get(:@@method_literals)['empty']
|
|
|
|
assert_evaluates_false(VariableLookup.parse('nil_value'), '==', empty_literal)
|
|
end
|
|
|
|
private
|
|
|
|
def assert_evaluates_true(left, op, right)
|
|
expr = BinaryExpression.new(left, op, right)
|
|
assert(
|
|
Condition.new(expr).evaluate(@context),
|
|
"Evaluated false: #{left.inspect} #{op} #{right.inspect}",
|
|
)
|
|
end
|
|
|
|
def assert_evaluates_false(left, op, right)
|
|
expr = BinaryExpression.new(left, op, right)
|
|
assert(
|
|
!Condition.new(expr).evaluate(@context),
|
|
"Evaluated true: #{left.inspect} #{op} #{right.inspect}",
|
|
)
|
|
end
|
|
|
|
def assert_evaluates_argument_error(left, op, right)
|
|
assert_raises(Liquid::ArgumentError) do
|
|
expr = BinaryExpression.new(left, op, right)
|
|
Condition.new(expr).evaluate(@context)
|
|
end
|
|
end
|
|
end # ConditionTest
|