dugway 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.bundle/config +2 -0
  3. data/.github/workflows/main.yml +1 -1
  4. data/.gitignore +1 -0
  5. data/README.md +32 -7
  6. data/dugway.gemspec +11 -9
  7. data/lib/dugway/application.rb +5 -3
  8. data/lib/dugway/assets/big_cartel_logo.svg +4 -0
  9. data/lib/dugway/cli/build.rb +7 -1
  10. data/lib/dugway/cli/server.rb +69 -9
  11. data/lib/dugway/cli/templates/source/settings.json +8 -0
  12. data/lib/dugway/cli/validate.rb +9 -2
  13. data/lib/dugway/controller.rb +5 -1
  14. data/lib/dugway/liquid/drops/account_drop.rb +4 -0
  15. data/lib/dugway/liquid/drops/artists_drop.rb +12 -0
  16. data/lib/dugway/liquid/drops/base_drop.rb +27 -2
  17. data/lib/dugway/liquid/drops/categories_drop.rb +12 -0
  18. data/lib/dugway/liquid/drops/features_drop.rb +144 -0
  19. data/lib/dugway/liquid/drops/pages_drop.rb +30 -2
  20. data/lib/dugway/liquid/drops/product_drop.rb +11 -0
  21. data/lib/dugway/liquid/drops/products_drop.rb +39 -0
  22. data/lib/dugway/liquid/drops/theme_drop.rb +52 -6
  23. data/lib/dugway/liquid/drops/translations_drop.rb +122 -0
  24. data/lib/dugway/liquid/filters/core_filters.rb +169 -7
  25. data/lib/dugway/liquid/filters/font_filters.rb +1 -0
  26. data/lib/dugway/liquid/filters/util_filters.rb +1 -10
  27. data/lib/dugway/liquid/tags/get.rb +6 -6
  28. data/lib/dugway/liquid/tags/paginate.rb +61 -11
  29. data/lib/dugway/liquifier.rb +44 -8
  30. data/lib/dugway/store.rb +46 -3
  31. data/lib/dugway/theme.rb +151 -15
  32. data/lib/dugway/version.rb +1 -1
  33. data/lib/dugway.rb +55 -2
  34. data/locales/storefront.de.yml +81 -0
  35. data/locales/storefront.en-CA.yml +81 -0
  36. data/locales/storefront.en-GB.yml +81 -0
  37. data/locales/storefront.en-US.yml +81 -0
  38. data/locales/storefront.es-ES.yml +81 -0
  39. data/locales/storefront.es-MX.yml +81 -0
  40. data/locales/storefront.fr-CA.yml +81 -0
  41. data/locales/storefront.fr-FR.yml +81 -0
  42. data/locales/storefront.id.yml +81 -0
  43. data/locales/storefront.it.yml +81 -0
  44. data/locales/storefront.ja.yml +81 -0
  45. data/locales/storefront.ko.yml +81 -0
  46. data/locales/storefront.nl.yml +81 -0
  47. data/locales/storefront.pl.yml +81 -0
  48. data/locales/storefront.pt-BR.yml +81 -0
  49. data/locales/storefront.pt-PT.yml +81 -0
  50. data/locales/storefront.ro.yml +81 -0
  51. data/locales/storefront.sv.yml +81 -0
  52. data/locales/storefront.tr.yml +81 -0
  53. data/locales/storefront.zh-CN.yml +81 -0
  54. data/locales/storefront.zh-TW.yml +81 -0
  55. data/log/dugway.log +1 -0
  56. data/mise.toml +2 -0
  57. data/spec/features/page_rendering_spec.rb +4 -4
  58. data/spec/fixtures/theme/layout.html +2 -0
  59. data/spec/fixtures/theme/settings.json +6 -0
  60. data/spec/spec_helper.rb +7 -0
  61. data/spec/units/dugway/liquid/drops/features_drop_spec.rb +182 -0
  62. data/spec/units/dugway/liquid/drops/pages_drop_spec.rb +186 -7
  63. data/spec/units/dugway/liquid/drops/product_drop_spec.rb +17 -0
  64. data/spec/units/dugway/liquid/drops/theme_drop_spec.rb +45 -0
  65. data/spec/units/dugway/liquid/drops/translations_drop_spec.rb +292 -0
  66. data/spec/units/dugway/liquid/filters/core_filters_spec.rb +301 -3
  67. data/spec/units/dugway/store_spec.rb +55 -0
  68. data/spec/units/dugway/theme_spec.rb +543 -1
  69. metadata +84 -25
@@ -0,0 +1,292 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dugway::Drops::TranslationsDrop do
4
+ let(:settings) { {} }
5
+ let(:definitions) { {} }
6
+ let(:drop) { Dugway::Drops::TranslationsDrop.new(settings, definitions) }
7
+
8
+ before(:all) do
9
+ # Store original I18n settings to restore them later
10
+ @original_i18n_load_path = I18n.load_path.dup
11
+ @original_i18n_available_locales = I18n.config.available_locales.dup
12
+ @original_i18n_default_locale = I18n.default_locale
13
+ @original_i18n_locale = I18n.locale
14
+
15
+ # Create a unique temporary directory for theme-specific locale files for this test run
16
+ @theme_parent_dir = File.expand_path("../../../../tmp/translations_drop_spec_#{Time.now.to_f}", __FILE__)
17
+ @theme_source_dir = File.join(@theme_parent_dir, 'source') # For Dugway.source_dir mock
18
+ @theme_locales_dir = File.join(@theme_parent_dir, '_dugway_locales') # Theme locale files go here
19
+
20
+ FileUtils.mkdir_p(@theme_source_dir)
21
+ FileUtils.mkdir_p(@theme_locales_dir)
22
+
23
+ # Write theme-specific test files. These will override or supplement gem locales.
24
+ # Using 'navigation.cart' as a key that exists in actual gem locales (e.g., storefront.en-US.yml)
25
+ # and 'farewell' as a theme-only key.
26
+ # We use 'en-US' for the theme file to directly override the gem's 'en-US' translations.
27
+ File.write(File.join(@theme_locales_dir, 'storefront.en-US.yml'), <<~YAML)
28
+ en-US:
29
+ storefront:
30
+ navigation:
31
+ cart: "Theme Cart" # Override gem's "Cart"
32
+ farewell: "Goodbye Theme" # Theme-only
33
+ YAML
34
+ File.write(File.join(@theme_locales_dir, 'storefront.fr.yml'), <<~YAML)
35
+ fr:
36
+ storefront:
37
+ navigation:
38
+ cart: "Panier du Thème"
39
+ YAML
40
+
41
+ # Force load gem locales first (mimicking initial gem load)
42
+ # The main lib/dugway.rb (required via spec_helper) should have already configured
43
+ # I18n.load_path and I18n.config.available_locales with the gem's actual files.
44
+ # We are capturing this initial state and then augmenting it for theme-specific test files.
45
+
46
+ @theme_locale_files = Dir.glob(File.join(@theme_locales_dir, 'storefront.*.yml'))
47
+
48
+ # Augment the load path: original gem paths + new theme paths.
49
+ # Theme files are added last so their definitions override gem's if keys conflict.
50
+ I18n.load_path = (@original_i18n_load_path + @theme_locale_files).uniq
51
+
52
+ # Augment available locales: original gem locales + new theme locales.
53
+ theme_locale_codes = @theme_locale_files.map do |file|
54
+ name = File.basename(file, '.yml')
55
+ locale_code = name.match(/^storefront\.(.+)$/)&.captures&.first
56
+ locale_code&.to_sym
57
+ end.compact
58
+
59
+ current_available_locales = (@original_i18n_available_locales.to_a + theme_locale_codes).uniq
60
+
61
+ if current_available_locales.any? { |loc| loc.to_s.start_with?('en-') } && !current_available_locales.include?(:en)
62
+ current_available_locales << :en
63
+ end
64
+
65
+ I18n.config.available_locales = current_available_locales
66
+
67
+ # Force a clean slate for translations before reloading.
68
+ # This ensures that the load path order is strictly respected for overrides.
69
+ I18n.backend.send(:init_translations) if I18n.backend.respond_to?(:init_translations, true)
70
+
71
+ # Reload the backend to process all files in the (now augmented) I18n.load_path
72
+ # and to recognize all locales in I18n.config.available_locales.
73
+ I18n.backend.reload!
74
+
75
+ # Verify that :'en-US' (which should come from the gem's original setup) is recognized.
76
+ # If this fails, it means :'en-US' was not in @original_i18n_available_locales or was lost.
77
+ unless I18n.config.available_locales.include?(:'en-US') && I18n.backend.available_locales.include?(:'en-US')
78
+ error_message = "I18n Setup Issue: :en-US is not available after setup.\n"
79
+ error_message += "I18n.config.available_locales: #{I18n.config.available_locales.inspect}\n"
80
+ error_message += "I18n.backend.available_locales: #{I18n.backend.available_locales.inspect}\n"
81
+ error_message += "I18n.load_path: #{I18n.load_path.inspect}\n"
82
+ error_message += "@original_i18n_available_locales: #{@original_i18n_available_locales.inspect}\n"
83
+ error_message += "@original_i18n_load_path: #{@original_i18n_load_path.inspect}"
84
+ raise error_message
85
+ end
86
+
87
+ I18n.default_locale = :'en-US'
88
+ I18n.locale = :'en-US'
89
+ end
90
+
91
+ after(:all) do
92
+ # Clean up temporary theme directory
93
+ FileUtils.rm_rf(@theme_parent_dir)
94
+
95
+ # Restore original I18n settings
96
+ I18n.load_path = @original_i18n_load_path
97
+ I18n.config.available_locales = @original_i18n_available_locales
98
+ I18n.default_locale = @original_i18n_default_locale
99
+ I18n.locale = @original_i18n_locale
100
+ I18n.backend.reload!
101
+ end
102
+ let(:logger_double) { double('Logger', warn: nil) }
103
+
104
+ before(:each) do
105
+ allow(Dugway).to receive(:gem_root).and_return(File.expand_path('../../../../../', __FILE__))
106
+ allow(Dugway).to receive(:source_dir).and_return(@theme_source_dir)
107
+
108
+ allow(Dugway).to receive(:logger).and_return(logger_double)
109
+ end
110
+
111
+ describe 'Automatic Mode (Default)' do
112
+ context "with default language ('en-US', falling back to 'en' for theme)" do
113
+ # These tests rely on I18n.locale being 'en-US' (set in before(:all))
114
+ # The theme provides 'storefront.en.yml'
115
+ it "returns theme override for a key also in gem locales" do
116
+ # Gem 'en-US' (locales/storefront.en-US.yml) has storefront.navigation.cart: "Cart"
117
+ # Theme 'en' (temporary file) has storefront.navigation.cart: "Theme Cart"
118
+ expect(drop['navigation.cart']).to eq('Theme Cart')
119
+ end
120
+
121
+ it "returns theme-only translation if present" do
122
+ # 'farewell' is only in the temporary theme 'en' file
123
+ expect(drop['farewell']).to eq('Goodbye Theme')
124
+ end
125
+
126
+ it "returns gem translation if not in theme" do
127
+ # 'navigation.all_products' is in gem 'en-US.yml', not in our test theme 'en.yml'
128
+ # The value from locales/storefront.en-US.yml is "All products"
129
+ expect(drop['navigation.all_products']).to eq('All products')
130
+ end
131
+
132
+ it "returns missing translation message for unknown key" do
133
+ expect(drop['unknown.key']).to eq('[MISSING TRANSLATION: unknown.key]')
134
+ end
135
+
136
+ it "logs a warning for missing translation with the correct locale" do
137
+ expect(Dugway.logger).to receive(:warn).with("Missing translation: key='unknown.key.again' locale='en-US' mode='automatic'")
138
+ drop['unknown.key.again']
139
+ end
140
+ end
141
+
142
+ context "with specified locale ('fr')" do
143
+ let(:settings) { { translation_locale: 'fr' } }
144
+
145
+ it "returns translation for the specified language from theme" do
146
+ # Theme 'fr' (temporary file) has storefront.navigation.cart: "Panier du Thème"
147
+ expect(drop['navigation.cart']).to eq('Panier du Thème')
148
+ end
149
+
150
+ it "returns missing translation message if key not found in specified language (and not in fallbacks)" do
151
+ # 'farewell' is not in theme 'fr.yml' or any 'fr' gem locale
152
+ expect(drop['farewell']).to eq('[MISSING TRANSLATION: farewell]')
153
+ end
154
+
155
+ it "returns gem translation if key not found in specified theme language but exists in gem's version of that language" do
156
+ # Assuming 'locales/storefront.fr-FR.yml' (loaded by gem) has 'navigation.all': 'Tous'
157
+ # and our temporary theme 'fr.yml' does not have 'navigation.all'
158
+ # This test depends on the content of the actual gem's fr-FR file.
159
+ # For a more robust test, ensure 'locales/storefront.fr-FR.yml' has 'navigation: { all: "Tous" }'
160
+ # Or, if 'fr-FR' is not a primary test target, this test could be more generic or skipped.
161
+ # For now, let's assume 'navigation.all' exists in a loaded 'fr' variant from the gem.
162
+ # If 'locales/storefront.fr-FR.yml' exists and is loaded:
163
+ # expect(drop['navigation.all']).to eq(I18n.t('storefront.navigation.all', locale: :fr))
164
+ # If not, it will be missing. Let's test for missing if not in theme 'fr'.
165
+ expect(drop['navigation.all_products']).to eq('[MISSING TRANSLATION: navigation.all_products]')
166
+ end
167
+
168
+ it "logs a warning with the correct locale" do
169
+ expect(Dugway.logger).to receive(:warn).with("Missing translation: key='farewell.again' locale='fr' mode='automatic'")
170
+ drop['farewell.again']
171
+ end
172
+ end
173
+
174
+ context "with specified non-existent locale ('xx')" do
175
+ let(:settings) { { translation_locale: 'xx' } }
176
+
177
+ it "returns missing translation message for a known key" do
178
+ expect(drop['navigation.cart']).to eq('[MISSING TRANSLATION: navigation.cart]')
179
+ end
180
+ end
181
+ end
182
+
183
+ describe 'Manual Mode' do
184
+ let(:settings) do
185
+ {
186
+ translation_mode: 'manual',
187
+ greeting_tr_text: 'Hello Manual',
188
+ 'nested_message_tr_text' => 'Manual Nested',
189
+ farewell_tr_text: '',
190
+ explicit_nil_tr_text: nil
191
+ }
192
+ end
193
+
194
+ it "returns translation from manual setting (symbol key)" do
195
+ expect(drop['greeting']).to eq('Hello Manual')
196
+ end
197
+
198
+ it "returns translation from manual setting (string key)" do
199
+ # Note: The drop converts the lookup key 'nested.message' to :nested_message_tr_text
200
+ # but the settings hash might have string keys from JSON parsing.
201
+ # The drop implementation checks both symbol and string keys in the settings hash.
202
+ expect(drop['nested.message']).to eq('Manual Nested')
203
+ end
204
+
205
+ it "falls back to automatic translation if manual setting is blank and allow_blank is false (default)" do
206
+ # No definition provided, so allow_blank defaults to false
207
+ # 'farewell' exists in theme 'en' locale (storefront.en.yml created by test)
208
+ # I18n.locale is 'en-US'. The drop will use this locale.
209
+ expect(I18n).to receive(:t).with('storefront.farewell', locale: 'en-US', default: nil).and_call_original
210
+ expect(drop['farewell']).to eq('Goodbye Theme')
211
+ end
212
+
213
+ it "returns missing translation message if manual setting is explicitly nil" do
214
+ # If the key exists in manual settings but the value is nil, treat it as missing.
215
+ expect(drop['explicit_nil']).to eq('[MISSING TRANSLATION: explicit_nil]')
216
+ end
217
+
218
+ it "returns missing translation message if manual setting is not present" do
219
+ expect(drop['unknown.key']).to eq('[MISSING TRANSLATION: unknown.key]')
220
+ end
221
+
222
+ it "logs a warning for missing translation (manual mode)" do
223
+ # Expect a call to the stubbed logger
224
+ expect(Dugway.logger).to receive(:warn).with("Missing translation: key='unknown.key.manual' mode='manual'")
225
+ drop['unknown.key.manual']
226
+ end
227
+
228
+ it "does not use I18n backend if a valid manual setting is found" do
229
+ expect(I18n).not_to receive(:t)
230
+ expect(drop['greeting']).to eq('Hello Manual') # Uses manual setting
231
+ end
232
+
233
+ it "uses I18n backend if manual setting is blank and allow_blank is false" do
234
+ expect(I18n).to receive(:t).with('storefront.farewell', locale: 'en-US', default: nil).and_call_original
235
+ drop['farewell'] # Trigger lookup for blank, disallowed value
236
+ end
237
+
238
+ it "uses I18n backend if manual setting key is not found" do
239
+ expect(I18n).to receive(:t).with('storefront.unknown.key', locale: 'en-US', default: nil).and_call_original
240
+ drop['unknown.key'] # Trigger lookup for missing key
241
+ end
242
+
243
+ context "with allow_blank definitions" do
244
+ let(:definitions) do
245
+ {
246
+ 'options' => [
247
+ { 'variable' => 'allow_blank_true_tr_text', 'allow_blank' => true },
248
+ { 'variable' => 'allow_blank_false_tr_text', 'allow_blank' => false },
249
+ { 'variable' => 'allow_blank_missing_tr_text' }, # allow_blank defaults to false
250
+ { 'variable' => 'allow_blank_non_blank_val_tr_text', 'allow_blank' => false }
251
+ ]
252
+ }
253
+ end
254
+ let(:settings) do
255
+ {
256
+ translation_mode: 'manual',
257
+ allow_blank_true_tr_text: '',
258
+ allow_blank_false_tr_text: '',
259
+ allow_blank_missing_tr_text: '',
260
+ allow_blank_non_blank_val_tr_text: 'Not Blank'
261
+ }
262
+ end
263
+
264
+ it "returns blank string when allow_blank is true" do
265
+ expect(drop['allow_blank_true']).to eq('')
266
+ end
267
+
268
+ it "falls back to automatic translation when allow_blank is false" do
269
+ # Assuming 'allow_blank_false' doesn't exist in automatic translations
270
+ expect(I18n).to receive(:t).with('storefront.allow_blank_false', locale: 'en-US', default: nil).and_return(nil)
271
+ expect(drop['allow_blank_false']).to eq('[MISSING TRANSLATION: allow_blank_false]')
272
+ end
273
+
274
+ it "falls back to automatic translation when allow_blank is missing (defaults to false)" do
275
+ # Assuming 'allow_blank_missing' doesn't exist in automatic translations
276
+ expect(I18n).to receive(:t).with('storefront.allow_blank_missing', locale: 'en-US', default: nil).and_return(nil)
277
+ expect(drop['allow_blank_missing']).to eq('[MISSING TRANSLATION: allow_blank_missing]')
278
+ end
279
+
280
+ it "returns non-blank value even when allow_blank is false" do
281
+ expect(drop['allow_blank_non_blank_val']).to eq('Not Blank')
282
+ end
283
+ end
284
+ end
285
+
286
+ describe '#derive_manual_setting_key' do
287
+ # Test the private method indirectly via its usage or directly if needed
288
+ it 'converts dot notation to underscore symbol notation' do
289
+ expect(drop.send(:derive_manual_setting_key, 'some.key.name')).to eq(:some_key_name_tr_text)
290
+ end
291
+ end
292
+ end
@@ -34,12 +34,251 @@ describe Dugway::Filters::CoreFilters do
34
34
  end
35
35
 
36
36
  it "should support the 'code' format argument" do
37
- rendered_template("{{ 1234.56 | money: 'code' }}").should == '1,234.56 <span class="currency_code">USD</span>'
37
+ rendered_template("{{ 1234.56 | money: 'code' }}").should == '<span class="currency_code">USD</span>1,234.56'
38
38
  end
39
39
 
40
40
  it "should support the 'sign_and_code' format argument" do
41
41
  rendered_template("{{ 1234.56 | money: 'sign_and_code' }}").should == '<span class="currency_sign">$</span>1,234.56 <span class="currency_code">USD</span>'
42
42
  end
43
+
44
+ it "should use theme.money_format when explicitly passed as parameter" do
45
+ theme_drop = Dugway::Drops::ThemeDrop.new({ 'money_format' => 'sign' }, {})
46
+ rendered_template("{{ 1234.56 | money: theme.money_format }}", { 'theme' => theme_drop }).should == '<span class="currency_sign">$</span>1,234.56'
47
+ end
48
+
49
+ it "should use theme.money_format = 'code' when explicitly passed as parameter" do
50
+ theme_drop = Dugway::Drops::ThemeDrop.new({ 'money_format' => 'code' }, {})
51
+ rendered_template("{{ 1234.56 | money: theme.money_format }}", { 'theme' => theme_drop }).should == '<span class="currency_code">USD</span>1,234.56'
52
+ end
53
+
54
+ it "should use theme.money_format = 'sign_and_code' when explicitly passed as parameter" do
55
+ theme_drop = Dugway::Drops::ThemeDrop.new({ 'money_format' => 'sign_and_code' }, {})
56
+ rendered_template("{{ 1234.56 | money: theme.money_format }}", { 'theme' => theme_drop }).should == '<span class="currency_sign">$</span>1,234.56 <span class="currency_code">USD</span>'
57
+ end
58
+
59
+ it "should use default format when no format argument is provided" do
60
+ theme_drop = Dugway::Drops::ThemeDrop.new({ 'money_format' => 'sign' }, {})
61
+ rendered_template("{{ 1234.56 | money }}", { 'theme' => theme_drop }).should == '1,234.56'
62
+ end
63
+
64
+ it "should fall back to default format when theme.money_format is not set" do
65
+ rendered_template("{{ 1234.56 | money }}").should == '1,234.56'
66
+ end
67
+
68
+ it "should handle money_format from dugway.json customization when explicitly passed" do
69
+ # Simulate how the theme would be loaded with dugway.json customization
70
+ customization = { 'money_format' => 'sign' }
71
+ theme_drop = Dugway::Drops::ThemeDrop.new(customization, {})
72
+ rendered_template("{{ 1234.56 | money: theme.money_format }}", { 'theme' => theme_drop }).should == '<span class="currency_sign">$</span>1,234.56'
73
+ end
74
+
75
+ it "should handle money: theme.money_format syntax like storefront" do
76
+ # Test the exact syntax the user is using: {{ price | money: theme.money_format }}
77
+ customization = { 'money_format' => 'sign' }
78
+ theme_drop = Dugway::Drops::ThemeDrop.new(customization, {})
79
+ rendered_template("{{ 1234.56 | money: theme.money_format }}", { 'theme' => theme_drop }).should == '<span class="currency_sign">$</span>1,234.56'
80
+ end
81
+
82
+ it "should handle money_format 'none' correctly returning just the number" do
83
+ customization = { 'money_format' => 'none' }
84
+ theme_drop = Dugway::Drops::ThemeDrop.new(customization, {})
85
+ rendered_template("{{ 1234.56 | money: theme.money_format }}", { 'theme' => theme_drop }).should == '1,234.56'
86
+ end
87
+
88
+ it "should support the 'rounded' format argument" do
89
+ rendered_template("{{ 1234.56 | money: 'rounded' }}").should == '1,235'
90
+ end
91
+
92
+ it "should support the 'sign_rounded' format argument" do
93
+ rendered_template("{{ 1234.56 | money: 'sign_rounded' }}").should == '<span class="currency_sign">$</span>1,235'
94
+ end
95
+
96
+ it "should support the 'code_rounded' format argument" do
97
+ rendered_template("{{ 1234.56 | money: 'code_rounded' }}").should == '<span class="currency_code">USD</span>1,235'
98
+ end
99
+
100
+ it "should round up 10.99 to 11 with rounded format" do
101
+ rendered_template("{{ 10.99 | money: 'rounded' }}").should == '11'
102
+ end
103
+
104
+ it "should round up 10.99 to $11 with sign_rounded format" do
105
+ rendered_template("{{ 10.99 | money: 'sign_rounded' }}").should == '<span class="currency_sign">$</span>11'
106
+ end
107
+
108
+ it "should round up 10.99 to 11 USD with code_rounded format" do
109
+ rendered_template("{{ 10.99 | money: 'code_rounded' }}").should == '<span class="currency_code">USD</span>11'
110
+ end
111
+
112
+ it "should handle exact amounts with rounded formats" do
113
+ rendered_template("{{ 10.00 | money: 'rounded' }}").should == '10'
114
+ rendered_template("{{ 10.00 | money: 'sign_rounded' }}").should == '<span class="currency_sign">$</span>10'
115
+ rendered_template("{{ 10.00 | money: 'code_rounded' }}").should == '<span class="currency_code">USD</span>10'
116
+ end
117
+ end
118
+
119
+ describe "#product_price" do
120
+ it "should handle fixed pricing products" do
121
+ product = { 'default_price' => 1234.56, 'variable_pricing' => false }
122
+ rendered_template("{{ product | product_price }}", { 'product' => product }).should == '1,234.56'
123
+ end
124
+
125
+ it "should handle fixed pricing products with format" do
126
+ product = { 'default_price' => 1234.56, 'variable_pricing' => false }
127
+ rendered_template("{{ product | product_price: 'sign' }}", { 'product' => product }).should == '<span class="currency_sign">$</span>1,234.56'
128
+ end
129
+
130
+ it "should handle variable pricing products with default format" do
131
+ product = { 'min_price' => 10.00, 'max_price' => 20.00, 'variable_pricing' => true }
132
+ rendered_template("{{ product | product_price }}", { 'product' => product }).should == '10.00 - 20.00'
133
+ end
134
+
135
+ it "should handle variable pricing products with sign format" do
136
+ product = { 'min_price' => 10.00, 'max_price' => 20.00, 'variable_pricing' => true }
137
+ rendered_template("{{ product | product_price: 'sign' }}", { 'product' => product }).should == '<span class="currency_sign">$</span>10.00 - <span class="currency_sign">$</span>20.00'
138
+ end
139
+
140
+ it "should handle variable pricing products with code format" do
141
+ product = { 'min_price' => 10.00, 'max_price' => 20.00, 'variable_pricing' => true }
142
+ rendered_template("{{ product | product_price: 'code' }}", { 'product' => product }).should == '<span class="currency_code">USD</span>10.00 - 20.00'
143
+ end
144
+
145
+ it "should return empty for nil product" do
146
+ rendered_template("{{ nil | product_price }}").should == ''
147
+ end
148
+
149
+ it "should fallback to price field if default_price is not present" do
150
+ product = { 'price' => 1234.56, 'variable_pricing' => false }
151
+ rendered_template("{{ product | product_price }}", { 'product' => product }).should == '1,234.56'
152
+ end
153
+
154
+ it "should work with theme.money_format for fixed pricing" do
155
+ product = { 'default_price' => 1234.56, 'variable_pricing' => false }
156
+ customization = { 'money_format' => 'sign' }
157
+ theme_drop = Dugway::Drops::ThemeDrop.new(customization, {})
158
+ rendered_template("{{ product | product_price: theme.money_format }}", { 'product' => product, 'theme' => theme_drop }).should == '<span class="currency_sign">$</span>1,234.56'
159
+ end
160
+
161
+ it "should work with theme.money_format for variable pricing" do
162
+ product = { 'min_price' => 10.00, 'max_price' => 20.00, 'variable_pricing' => true }
163
+ customization = { 'money_format' => 'sign' }
164
+ theme_drop = Dugway::Drops::ThemeDrop.new(customization, {})
165
+ rendered_template("{{ product | product_price: theme.money_format }}", { 'product' => product, 'theme' => theme_drop }).should == '<span class="currency_sign">$</span>10.00 - <span class="currency_sign">$</span>20.00'
166
+ end
167
+
168
+ it "should handle locale-specific currency formatting" do
169
+ # Test that the currency code position respects locale formatting
170
+ product = { 'min_price' => 10.00, 'max_price' => 20.00, 'variable_pricing' => true }
171
+ rendered_template("{{ product | product_price: 'code' }}", { 'product' => product }).should == '<span class="currency_code">USD</span>10.00 - 20.00'
172
+ end
173
+
174
+ # Tests for range_format parameter
175
+ it "should show min price only when range_format is 'min_only'" do
176
+ product = { 'min_price' => 10.00, 'max_price' => 20.00, 'variable_pricing' => true }
177
+ rendered_template("{{ product | product_price: 'sign', 'min_only' }}", { 'product' => product }).should == '<span class="currency_sign">$</span>10.00'
178
+ end
179
+
180
+ it "should show min price only with code format when range_format is 'min_only'" do
181
+ product = { 'min_price' => 10.00, 'max_price' => 20.00, 'variable_pricing' => true }
182
+ rendered_template("{{ product | product_price: 'code', 'min_only' }}", { 'product' => product }).should == '<span class="currency_code">USD</span>10.00'
183
+ end
184
+
185
+ it "should show max price only when range_format is 'max_only'" do
186
+ product = { 'min_price' => 10.00, 'max_price' => 20.00, 'variable_pricing' => true }
187
+ rendered_template("{{ product | product_price: 'sign', 'max_only' }}", { 'product' => product }).should == '<span class="currency_sign">$</span>20.00'
188
+ end
189
+
190
+ it "should show max price only with code format when range_format is 'max_only'" do
191
+ product = { 'min_price' => 10.00, 'max_price' => 20.00, 'variable_pricing' => true }
192
+ rendered_template("{{ product | product_price: 'code', 'max_only' }}", { 'product' => product }).should == '<span class="currency_code">USD</span>20.00'
193
+ end
194
+
195
+ it "should show range when range_format is 'default'" do
196
+ product = { 'min_price' => 10.00, 'max_price' => 20.00, 'variable_pricing' => true }
197
+ rendered_template("{{ product | product_price: 'sign', 'default' }}", { 'product' => product }).should == '<span class="currency_sign">$</span>10.00 - <span class="currency_sign">$</span>20.00'
198
+ end
199
+
200
+ it "should show range when range_format is nil" do
201
+ product = { 'min_price' => 10.00, 'max_price' => 20.00, 'variable_pricing' => true }
202
+ rendered_template("{{ product | product_price: 'sign', nil }}", { 'product' => product }).should == '<span class="currency_sign">$</span>10.00 - <span class="currency_sign">$</span>20.00'
203
+ end
204
+
205
+ it "should show range when range_format is an invalid value" do
206
+ product = { 'min_price' => 10.00, 'max_price' => 20.00, 'variable_pricing' => true }
207
+ rendered_template("{{ product | product_price: 'sign', 'invalid' }}", { 'product' => product }).should == '<span class="currency_sign">$</span>10.00 - <span class="currency_sign">$</span>20.00'
208
+ end
209
+
210
+ it "should show range by default when range_format is not specified" do
211
+ product = { 'min_price' => 10.00, 'max_price' => 20.00, 'variable_pricing' => true }
212
+ rendered_template("{{ product | product_price: 'sign' }}", { 'product' => product }).should == '<span class="currency_sign">$</span>10.00 - <span class="currency_sign">$</span>20.00'
213
+ end
214
+
215
+ # Test ThemeDrop integration for real-world usage
216
+ it "should work with theme-based range format when 'min_only'" do
217
+ product = { 'min_price' => 10.00, 'max_price' => 20.00, 'variable_pricing' => true }
218
+ customization = { 'price_range_format' => 'min_only' }
219
+ theme_drop = Dugway::Drops::ThemeDrop.new(customization, {})
220
+ rendered_template("{{ product | product_price: 'sign', theme.price_range_format }}", { 'product' => product, 'theme' => theme_drop }).should == '<span class="currency_sign">$</span>10.00'
221
+ end
222
+
223
+ it "should work with theme-based range format when 'max_only'" do
224
+ product = { 'min_price' => 10.00, 'max_price' => 20.00, 'variable_pricing' => true }
225
+ customization = { 'price_range_format' => 'max_only' }
226
+ theme_drop = Dugway::Drops::ThemeDrop.new(customization, {})
227
+ rendered_template("{{ product | product_price: 'sign', theme.price_range_format }}", { 'product' => product, 'theme' => theme_drop }).should == '<span class="currency_sign">$</span>20.00'
228
+ end
229
+
230
+ it "should work with both theme.money_format and theme-based range format" do
231
+ product = { 'min_price' => 10.00, 'max_price' => 20.00, 'variable_pricing' => true }
232
+ customization = { 'money_format' => 'sign', 'price_range_format' => 'min_only' }
233
+ theme_drop = Dugway::Drops::ThemeDrop.new(customization, {})
234
+ rendered_template("{{ product | product_price: theme.money_format, theme.price_range_format }}", { 'product' => product, 'theme' => theme_drop }).should == '<span class="currency_sign">$</span>10.00'
235
+ end
236
+
237
+ # Tests for rounded formats
238
+ it "should handle fixed pricing products with rounded format" do
239
+ product = { 'default_price' => 1234.56, 'variable_pricing' => false }
240
+ rendered_template("{{ product | product_price: 'rounded' }}", { 'product' => product }).should == '1,235'
241
+ end
242
+
243
+ it "should handle fixed pricing products with sign_rounded format" do
244
+ product = { 'default_price' => 1234.56, 'variable_pricing' => false }
245
+ rendered_template("{{ product | product_price: 'sign_rounded' }}", { 'product' => product }).should == '<span class="currency_sign">$</span>1,235'
246
+ end
247
+
248
+ it "should handle fixed pricing products with code_rounded format" do
249
+ product = { 'default_price' => 1234.56, 'variable_pricing' => false }
250
+ rendered_template("{{ product | product_price: 'code_rounded' }}", { 'product' => product }).should == '<span class="currency_code">USD</span>1,235'
251
+ end
252
+
253
+ it "should handle variable pricing products with rounded format" do
254
+ product = { 'min_price' => 10.99, 'max_price' => 20.99, 'variable_pricing' => true }
255
+ rendered_template("{{ product | product_price: 'rounded' }}", { 'product' => product }).should == '11 - 21'
256
+ end
257
+
258
+ it "should handle variable pricing products with sign_rounded format" do
259
+ product = { 'min_price' => 10.99, 'max_price' => 20.99, 'variable_pricing' => true }
260
+ rendered_template("{{ product | product_price: 'sign_rounded' }}", { 'product' => product }).should == '<span class="currency_sign">$</span>11 - <span class="currency_sign">$</span>21'
261
+ end
262
+
263
+ it "should handle variable pricing products with code_rounded format" do
264
+ product = { 'min_price' => 10.99, 'max_price' => 20.99, 'variable_pricing' => true }
265
+ rendered_template("{{ product | product_price: 'code_rounded' }}", { 'product' => product }).should == '<span class="currency_code">USD</span>11 - 21'
266
+ end
267
+
268
+ it "should handle rounded formats with range_format min_only" do
269
+ product = { 'min_price' => 10.99, 'max_price' => 20.99, 'variable_pricing' => true }
270
+ rendered_template("{{ product | product_price: 'sign_rounded', 'min_only' }}", { 'product' => product }).should == '<span class="currency_sign">$</span>11'
271
+ end
272
+
273
+ it "should handle rounded formats with range_format max_only" do
274
+ product = { 'min_price' => 10.99, 'max_price' => 20.99, 'variable_pricing' => true }
275
+ rendered_template("{{ product | product_price: 'code_rounded', 'max_only' }}", { 'product' => product }).should == '<span class="currency_code">USD</span>21'
276
+ end
277
+
278
+ it "should handle exact amounts with rounded variable pricing" do
279
+ product = { 'min_price' => 10.00, 'max_price' => 20.00, 'variable_pricing' => true }
280
+ rendered_template("{{ product | product_price: 'sign_rounded' }}", { 'product' => product }).should == '<span class="currency_sign">$</span>10 - <span class="currency_sign">$</span>20'
281
+ end
43
282
  end
44
283
 
45
284
  describe "#money_with_sign" do
@@ -50,7 +289,62 @@ describe Dugway::Filters::CoreFilters do
50
289
 
51
290
  describe "#money_with_code" do
52
291
  it "should convert a number to currency format with a code" do
53
- rendered_template("{{ 1234.56 | money_with_code }}").should == '1,234.56 <span class="currency_code">USD</span>'
292
+ rendered_template("{{ 1234.56 | money_with_code }}").should == '<span class="currency_code">USD</span>1,234.56'
293
+ end
294
+
295
+ # Tests for different currencies to ensure locale-specific formatting
296
+ context "with EUR currency" do
297
+ # TODO: The 'eu' locale is invalid and needs to be fixed in the bigcartel-currency-locales gem.
298
+ # Once the gem is updated to use a valid locale like 'de-DE' for EUR, we can update these tests
299
+ # to expect the correct European number formatting (1.200,99 instead of 1,200.99).
300
+ # For now, we're expecting the fallback US formatting due to the invalid locale.
301
+ it "should display currency code after number with space for EUR" do
302
+ eur_currency = { 'sign' => '€', 'name' => 'Euro', 'id' => 4, 'code' => 'EUR', 'locale' => 'eu' }
303
+ # Expected: '1.200,99 <span class="currency_code">EUR</span>' once locale is fixed
304
+ rendered_template_with_currency("{{ 1200.99 | money_with_code }}", eur_currency).should == '1,200.99 <span class="currency_code">EUR</span>'
305
+ end
306
+
307
+ it "should work with money: 'code' format for EUR" do
308
+ eur_currency = { 'sign' => '€', 'name' => 'Euro', 'id' => 4, 'code' => 'EUR', 'locale' => 'eu' }
309
+ # Expected: '1.200,99 <span class="currency_code">EUR</span>' once locale is fixed
310
+ rendered_template_with_currency("{{ 1200.99 | money: 'code' }}", eur_currency).should == '1,200.99 <span class="currency_code">EUR</span>'
311
+ end
312
+ end
313
+
314
+ context "with CHF currency" do
315
+ it "should display currency code before number with space for CHF" do
316
+ chf_currency = { 'sign' => 'CHF', 'name' => 'Swiss Franc', 'id' => 10, 'code' => 'CHF', 'locale' => 'gsw-CH' }
317
+ rendered_template_with_currency("{{ 1200.99 | money_with_code }}", chf_currency).should == '<span class="currency_code">CHF</span> 1\'200.99'
318
+ end
319
+
320
+ it "should work with money: 'code' format for CHF" do
321
+ chf_currency = { 'sign' => 'CHF', 'name' => 'Swiss Franc', 'id' => 10, 'code' => 'CHF', 'locale' => 'gsw-CH' }
322
+ rendered_template_with_currency("{{ 1200.99 | money: 'code' }}", chf_currency).should == '<span class="currency_code">CHF</span> 1\'200.99'
323
+ end
324
+ end
325
+
326
+ context "with JPY currency" do
327
+ it "should display currency code before number with no decimals for JPY" do
328
+ jpy_currency = { 'sign' => '¥', 'name' => 'Japanese Yen', 'id' => 6, 'code' => 'JPY', 'locale' => 'ja' }
329
+ rendered_template_with_currency("{{ 1200.99 | money_with_code }}", jpy_currency).should == '<span class="currency_code">JPY</span>1,201'
330
+ end
331
+
332
+ it "should work with money: 'code' format for JPY" do
333
+ jpy_currency = { 'sign' => '¥', 'name' => 'Japanese Yen', 'id' => 6, 'code' => 'JPY', 'locale' => 'ja' }
334
+ rendered_template_with_currency("{{ 1200.99 | money: 'code' }}", jpy_currency).should == '<span class="currency_code">JPY</span>1,201'
335
+ end
336
+ end
337
+
338
+ context "with GBP currency" do
339
+ it "should display currency code before number with no space for GBP" do
340
+ gbp_currency = { 'sign' => '£', 'name' => 'Pound Sterling', 'id' => 5, 'code' => 'GBP', 'locale' => 'en-GB' }
341
+ rendered_template_with_currency("{{ 1200.99 | money_with_code }}", gbp_currency).should == '<span class="currency_code">GBP</span>1,200.99'
342
+ end
343
+
344
+ it "should work with money: 'code' format for GBP" do
345
+ gbp_currency = { 'sign' => '£', 'name' => 'Pound Sterling', 'id' => 5, 'code' => 'GBP', 'locale' => 'en-GB' }
346
+ rendered_template_with_currency("{{ 1200.99 | money: 'code' }}", gbp_currency).should == '<span class="currency_code">GBP</span>1,200.99'
347
+ end
54
348
  end
55
349
  end
56
350
 
@@ -63,6 +357,10 @@ describe Dugway::Filters::CoreFilters do
63
357
  private
64
358
 
65
359
  def rendered_template(template, assigns={})
66
- Liquid::Template.parse(template).render(assigns, :registers => { :currency => Dugway.store.currency })
360
+ Liquid::Template.parse(template).render(assigns, :registers => { :currency => Dugway.store.currency, :settings => {} })
361
+ end
362
+
363
+ def rendered_template_with_currency(template, currency, assigns={})
364
+ Liquid::Template.parse(template).render(assigns, :registers => { :currency => currency, :settings => {} })
67
365
  end
68
366
  end