mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-12 23:40:45 -07:00
Merge pull request #2034 from Shopify/fix-liquid-spec-without-activesupport
Require liquid-spec to be run on commit automatically and related fixes
This commit is contained in:
@@ -9,26 +9,29 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
entry:
|
||||
- { ruby: 3.0, allowed-failure: false } # minimum supported
|
||||
- { ruby: 3.2, allowed-failure: false }
|
||||
- { ruby: 3.3, allowed-failure: false }
|
||||
- { ruby: 3.3, allowed-failure: false }
|
||||
- { ruby: 3.4, allowed-failure: false } # latest
|
||||
- { ruby: 3.3, allowed-failure: false } # minimum supported
|
||||
- { ruby: 3.4, allowed-failure: false, rubyopt: "--yjit" }
|
||||
- { ruby: 4.0, allowed-failure: false } # latest stable
|
||||
- {
|
||||
ruby: 3.4,
|
||||
ruby: 4.0,
|
||||
allowed-failure: false,
|
||||
rubyopt: "--enable-frozen-string-literal",
|
||||
}
|
||||
- { ruby: 3.4, allowed-failure: false, rubyopt: "--yjit" }
|
||||
- { ruby: head, allowed-failure: false }
|
||||
- { ruby: 4.0, allowed-failure: false, rubyopt: "--yjit" }
|
||||
- { ruby: 4.0, allowed-failure: false, rubyopt: "--zjit" }
|
||||
|
||||
# Head can have failures due to being in development
|
||||
- { ruby: head, allowed-failure: true }
|
||||
- {
|
||||
ruby: head,
|
||||
allowed-failure: false,
|
||||
allowed-failure: true,
|
||||
rubyopt: "--enable-frozen-string-literal",
|
||||
}
|
||||
- { ruby: head, allowed-failure: false, rubyopt: "--yjit" }
|
||||
- { ruby: head, allowed-failure: true, rubyopt: "--yjit" }
|
||||
- { ruby: head, allowed-failure: true, rubyopt: "--zjit" }
|
||||
name: Test Ruby ${{ matrix.entry.ruby }}
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
@@ -42,6 +45,23 @@ jobs:
|
||||
env:
|
||||
RUBYOPT: ${{ matrix.entry.rubyopt }}
|
||||
|
||||
spec:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
BUNDLE_WITH: spec
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
- uses: ruby/setup-ruby@a25f1e45f0e65a92fcb1e95e8847f78fb0a7197a # v1.273.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
bundler: latest
|
||||
- name: Run liquid-spec for all adapters
|
||||
run: |
|
||||
for adapter in spec/*.rb; do
|
||||
echo "=== Running $adapter ==="
|
||||
bundle exec liquid-spec run "$adapter" --no-max-failures
|
||||
done
|
||||
|
||||
memory_profile:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
+10
-1
@@ -174,7 +174,16 @@ Style/WordArray:
|
||||
|
||||
# Offense count: 117
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns.
|
||||
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, AllowCopDirectives, AllowedPatterns.
|
||||
# URISchemes: http, https
|
||||
Layout/LineLength:
|
||||
Max: 260
|
||||
|
||||
Naming/PredicatePrefix:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 1
|
||||
# This is intentional - early return from begin/rescue in assignment context
|
||||
Lint/NoReturnInBeginEndBlocks:
|
||||
Exclude:
|
||||
- 'lib/liquid/standardfilters.rb'
|
||||
|
||||
@@ -25,7 +25,14 @@ group :development do
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem 'rubocop', '~> 1.61.0'
|
||||
gem 'rubocop-shopify', '~> 2.12.0', require: false
|
||||
gem 'benchmark'
|
||||
gem 'rubocop', '~> 1.82.0'
|
||||
gem 'rubocop-shopify', '~> 2.18.0', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
end
|
||||
|
||||
group :spec do
|
||||
# Using feature branch until https://github.com/Shopify/liquid-spec/pull/97 is merged
|
||||
gem 'liquid-spec', github: 'Shopify/liquid-spec', branch: 'add-per-spec-required-features'
|
||||
gem 'activesupport', require: false
|
||||
end
|
||||
|
||||
+51
-12
@@ -113,24 +113,63 @@ module Liquid
|
||||
|
||||
def equal_variables(left, right)
|
||||
if left.is_a?(MethodLiteral)
|
||||
if right.respond_to?(left.method_name)
|
||||
return right.send(left.method_name)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
return call_method_literal(left, right)
|
||||
end
|
||||
|
||||
if right.is_a?(MethodLiteral)
|
||||
if left.respond_to?(right.method_name)
|
||||
return left.send(right.method_name)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
return call_method_literal(right, left)
|
||||
end
|
||||
|
||||
left == right
|
||||
end
|
||||
|
||||
def call_method_literal(literal, value)
|
||||
method_name = literal.method_name
|
||||
|
||||
# If the object responds to the method, use it
|
||||
if value.respond_to?(method_name)
|
||||
return value.send(method_name)
|
||||
end
|
||||
|
||||
# Implement blank?/empty? for common types that don't have it
|
||||
# (ActiveSupport adds these, but Liquid should work without it)
|
||||
case method_name
|
||||
when :blank?
|
||||
liquid_blank?(value)
|
||||
when :empty?
|
||||
liquid_empty?(value)
|
||||
end
|
||||
end
|
||||
|
||||
# Implement blank? semantics matching ActiveSupport
|
||||
def liquid_blank?(value)
|
||||
case value
|
||||
when NilClass, FalseClass
|
||||
true
|
||||
when TrueClass, Numeric
|
||||
false
|
||||
when String
|
||||
# Blank if empty or whitespace only
|
||||
value.empty? || value.match?(/\A\s*\z/)
|
||||
when Array, Hash
|
||||
value.empty?
|
||||
else
|
||||
# Fall back to empty? if available, otherwise false
|
||||
value.respond_to?(:empty?) ? value.empty? : false
|
||||
end
|
||||
end
|
||||
|
||||
# Implement empty? semantics
|
||||
# Note: nil is NOT empty (but IS blank). empty? checks if a collection has zero elements.
|
||||
def liquid_empty?(value)
|
||||
case value
|
||||
when String, Array, Hash
|
||||
value.empty?
|
||||
else
|
||||
value.respond_to?(:empty?) ? value.empty? : false
|
||||
end
|
||||
end
|
||||
|
||||
def interpret_condition(left, right, op, context)
|
||||
# If the operator is empty this means that the decision statement is just
|
||||
# a single variable. We can just poll this variable from the context and
|
||||
@@ -154,8 +193,8 @@ module Liquid
|
||||
end
|
||||
|
||||
def deprecated_default_context
|
||||
warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \
|
||||
" and will be removed from Liquid 6.0.0.")
|
||||
warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated " \
|
||||
"and will be removed from Liquid 6.0.0.")
|
||||
Context.new
|
||||
end
|
||||
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ module Liquid
|
||||
|
||||
# Catch all for the method
|
||||
def liquid_method_missing(method)
|
||||
return nil unless @context&.strict_variables
|
||||
return unless @context&.strict_variables
|
||||
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
|
||||
end
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def inner_parse(markup, ss, cache)
|
||||
if (markup.start_with?("(") && markup.end_with?(")")) && markup =~ RANGES_REGEX
|
||||
if markup.start_with?("(") && markup.end_with?(")") && markup =~ RANGES_REGEX
|
||||
return RangeLookup.parse(
|
||||
Regexp.last_match(1),
|
||||
Regexp.last_match(2),
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ module Liquid
|
||||
def interpolate(name, vars)
|
||||
name.gsub(/%\{(\w+)\}/) do
|
||||
# raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
|
||||
(vars[Regexp.last_match(1).to_sym]).to_s
|
||||
vars[Regexp.last_match(1).to_sym].to_s
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -768,6 +768,8 @@ module Liquid
|
||||
# @liquid_syntax array | first
|
||||
# @liquid_return [untyped]
|
||||
def first(array)
|
||||
# ActiveSupport returns "" for empty strings, not nil
|
||||
return array[0] || "" if array.is_a?(String)
|
||||
array.first if array.respond_to?(:first)
|
||||
end
|
||||
|
||||
@@ -779,6 +781,8 @@ module Liquid
|
||||
# @liquid_syntax array | last
|
||||
# @liquid_return [untyped]
|
||||
def last(array)
|
||||
# ActiveSupport returns "" for empty strings, not nil
|
||||
return array[-1] || "" if array.is_a?(String)
|
||||
array.last if array.respond_to?(:last)
|
||||
end
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ module Liquid
|
||||
byte_a = byte_b = @ss.scan_byte
|
||||
|
||||
while byte_b
|
||||
byte_a = @ss.scan_byte while byte_a && (byte_a != CLOSE_CURLEY && byte_a != OPEN_CURLEY)
|
||||
byte_a = @ss.scan_byte while byte_a && byte_a != CLOSE_CURLEY && byte_a != OPEN_CURLEY
|
||||
|
||||
break unless byte_a
|
||||
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ module Liquid
|
||||
return obj if obj.respond_to?(:strftime)
|
||||
|
||||
if obj.is_a?(String)
|
||||
return nil if obj.empty?
|
||||
return if obj.empty?
|
||||
obj = obj.downcase
|
||||
end
|
||||
|
||||
|
||||
@@ -70,6 +70,11 @@ module Liquid
|
||||
elsif lookup_command?(i) && object.respond_to?(key)
|
||||
object = object.send(key).to_liquid
|
||||
|
||||
# Handle string first/last like ActiveSupport does (returns first/last character)
|
||||
# ActiveSupport returns "" for empty strings, not nil
|
||||
elsif lookup_command?(i) && object.is_a?(String) && (key == "first" || key == "last")
|
||||
object = key == "first" ? (object[0] || "") : (object[-1] || "")
|
||||
|
||||
# No key was present with the desired value and it wasn't one of the directly supported
|
||||
# keywords either. The only thing we got left is to return nil or
|
||||
# raise an exception if `strict_variables` option is set to true
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Liquid Spec Adapter for Shopify/liquid (Ruby reference implementation)
|
||||
#
|
||||
# Run with: bundle exec liquid-spec run spec/ruby_liquid.rb
|
||||
|
||||
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
||||
require 'liquid'
|
||||
|
||||
LiquidSpec.configure do |config|
|
||||
# Run core Liquid specs
|
||||
config.features = [:core]
|
||||
end
|
||||
|
||||
# Compile a template string into a Liquid::Template
|
||||
LiquidSpec.compile do |source, options|
|
||||
Liquid::Template.parse(source, **options)
|
||||
end
|
||||
|
||||
# Render a compiled template with the given context
|
||||
# @param template [Liquid::Template] compiled template
|
||||
# @param assigns [Hash] environment variables
|
||||
# @param options [Hash] :registers, :strict_errors, :exception_renderer
|
||||
LiquidSpec.render do |template, assigns, options|
|
||||
registers = Liquid::Registers.new(options[:registers] || {})
|
||||
|
||||
context = Liquid::Context.build(
|
||||
static_environments: assigns,
|
||||
registers: registers,
|
||||
rethrow_errors: options[:strict_errors],
|
||||
)
|
||||
|
||||
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
|
||||
|
||||
template.render(context)
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Liquid Spec Adapter for Shopify/liquid with ActiveSupport loaded
|
||||
#
|
||||
# Run with: bundle exec liquid-spec run spec/ruby_liquid_with_active_support.rb
|
||||
|
||||
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
||||
require 'active_support/all'
|
||||
require 'liquid'
|
||||
|
||||
LiquidSpec.configure do |config|
|
||||
# Run core Liquid specs plus ActiveSupport SafeBuffer tests
|
||||
config.features = [:core, :activesupport]
|
||||
end
|
||||
|
||||
# Compile a template string into a Liquid::Template
|
||||
LiquidSpec.compile do |source, options|
|
||||
Liquid::Template.parse(source, **options)
|
||||
end
|
||||
|
||||
# Render a compiled template with the given context
|
||||
# @param template [Liquid::Template] compiled template
|
||||
# @param assigns [Hash] environment variables
|
||||
# @param options [Hash] :registers, :strict_errors, :exception_renderer
|
||||
LiquidSpec.render do |template, assigns, options|
|
||||
registers = Liquid::Registers.new(options[:registers] || {})
|
||||
|
||||
context = Liquid::Context.build(
|
||||
static_environments: assigns,
|
||||
registers: registers,
|
||||
rethrow_errors: options[:strict_errors],
|
||||
)
|
||||
|
||||
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
|
||||
|
||||
template.render(context)
|
||||
end
|
||||
@@ -59,7 +59,7 @@ class SecurityTest < Minitest::Test
|
||||
|
||||
GC.start
|
||||
|
||||
assert_equal([], (Symbol.all_symbols - current_symbols))
|
||||
assert_equal([], Symbol.all_symbols - current_symbols)
|
||||
end
|
||||
|
||||
def test_does_not_add_drop_methods_to_symbol_table
|
||||
@@ -70,7 +70,7 @@ class SecurityTest < Minitest::Test
|
||||
assert_equal("", Template.parse("{{ drop.custom_method_2 }}", assigns).render!)
|
||||
assert_equal("", Template.parse("{{ drop.custom_method_3 }}", assigns).render!)
|
||||
|
||||
assert_equal([], (Symbol.all_symbols - current_symbols))
|
||||
assert_equal([], Symbol.all_symbols - current_symbols)
|
||||
end
|
||||
|
||||
def test_max_depth_nested_blocks_does_not_raise_exception
|
||||
|
||||
@@ -116,7 +116,7 @@ class StandardFiltersTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_slice_on_arrays
|
||||
input = 'foobar'.split(//)
|
||||
input = 'foobar'.split('')
|
||||
assert_equal(%w(o o b), @filters.slice(input, 1, 3))
|
||||
assert_equal(%w(o o b a r), @filters.slice(input, 1, 1000))
|
||||
assert_equal(%w(), @filters.slice(input, 1, 0))
|
||||
@@ -627,6 +627,40 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_nil(@filters.last([]))
|
||||
end
|
||||
|
||||
def test_first_last_on_strings
|
||||
# Ruby's String class does not have first/last methods by default.
|
||||
# ActiveSupport adds String#first and String#last to return the first/last character.
|
||||
# Liquid must work without ActiveSupport, so the first/last filters handle strings specially.
|
||||
#
|
||||
# This enables template patterns like:
|
||||
# {{ product.title | first }} => "S" (for "Snowboard")
|
||||
# {{ customer.name | last }} => "h" (for "Smith")
|
||||
#
|
||||
# Note: ActiveSupport returns "" for empty strings, not nil.
|
||||
assert_equal('f', @filters.first('foo'))
|
||||
assert_equal('o', @filters.last('foo'))
|
||||
assert_equal('', @filters.first(''))
|
||||
assert_equal('', @filters.last(''))
|
||||
end
|
||||
|
||||
def test_first_last_on_unicode_strings
|
||||
# Unicode strings should return the first/last grapheme cluster (character),
|
||||
# not the first/last byte. Ruby's String#[] handles this correctly with index 0/-1.
|
||||
# This ensures international text works properly:
|
||||
# {{ korean_name | first }} => "고" (not a partial byte sequence)
|
||||
assert_equal('고', @filters.first('고스트빈'))
|
||||
assert_equal('빈', @filters.last('고스트빈'))
|
||||
end
|
||||
|
||||
def test_first_last_on_strings_via_template
|
||||
# Integration test to verify the filter works end-to-end in templates.
|
||||
# Empty strings return empty output (nil renders as empty string).
|
||||
assert_template_result('f', '{{ name | first }}', { 'name' => 'foo' })
|
||||
assert_template_result('o', '{{ name | last }}', { 'name' => 'foo' })
|
||||
assert_template_result('', '{{ name | first }}', { 'name' => '' })
|
||||
assert_template_result('', '{{ name | last }}', { 'name' => '' })
|
||||
end
|
||||
|
||||
def test_replace
|
||||
assert_equal('b b b b', @filters.replace('a a a a', 'a', 'b'))
|
||||
assert_equal('2 2 2 2', @filters.replace('1 1 1 1', 1, 2))
|
||||
@@ -1296,7 +1330,7 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_equal(1, @filters.sum(input, true))
|
||||
assert_equal(0.2, @filters.sum(input, 1.0))
|
||||
assert_equal(-0.3, @filters.sum(input, 1))
|
||||
assert_equal(0.4, @filters.sum(input, (1..5)))
|
||||
assert_equal(0.4, @filters.sum(input, 1..5))
|
||||
assert_equal(0, @filters.sum(input, nil))
|
||||
assert_equal(0, @filters.sum(input, ""))
|
||||
end
|
||||
|
||||
@@ -117,7 +117,7 @@ class StandardTagTest < Minitest::Test
|
||||
assigns = { 'condition' => "bad string here" }
|
||||
assert_template_result(
|
||||
'',
|
||||
'{% case condition %}{% when "string here" %} hit {% endcase %}',\
|
||||
'{% case condition %}{% when "string here" %} hit {% endcase %}',
|
||||
assigns,
|
||||
)
|
||||
end
|
||||
|
||||
@@ -133,7 +133,7 @@ class TemplateTest < Minitest::Test
|
||||
assert(t.resource_limits.reached?)
|
||||
|
||||
t.resource_limits.render_score_limit = 200
|
||||
assert_equal((" foo " * 100), t.render!)
|
||||
assert_equal(" foo " * 100, t.render!)
|
||||
refute_nil(t.resource_limits.render_score)
|
||||
end
|
||||
|
||||
|
||||
@@ -161,8 +161,8 @@ class ConditionUnitTest < Minitest::Test
|
||||
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."
|
||||
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
|
||||
|
||||
@@ -197,6 +197,172 @@ class ConditionUnitTest < Minitest::Test
|
||||
assert_equal(['title'], result.lookups)
|
||||
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.new('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.new('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.new('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.new('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.new('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.new('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.new('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.new('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.new('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.new('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.new('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.new('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.new('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.new('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.new('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.new('nil_value'), '==', empty_literal)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assert_evaluates_true(left, op, right)
|
||||
|
||||
@@ -8,7 +8,7 @@ class StrainerTemplateUnitTest < Minitest::Test
|
||||
def test_add_filter_when_wrong_filter_class
|
||||
c = Context.new
|
||||
s = c.strainer
|
||||
wrong_filter = ->(v) { v.reverse }
|
||||
wrong_filter = lambda(&:reverse)
|
||||
|
||||
exception = assert_raises(TypeError) do
|
||||
s.class.add_filter(wrong_filter)
|
||||
|
||||
Reference in New Issue
Block a user