dugway 1.0.14 → 1.2.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 (58) 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 +9 -0
  6. data/lib/dugway/application.rb +5 -3
  7. data/lib/dugway/assets/big_cartel_logo.svg +4 -0
  8. data/lib/dugway/cli/build.rb +18 -1
  9. data/lib/dugway/cli/server.rb +2 -2
  10. data/lib/dugway/cli/templates/source/settings.json +8 -0
  11. data/lib/dugway/cli/validate.rb +20 -2
  12. data/lib/dugway/controller.rb +5 -1
  13. data/lib/dugway/liquid/drops/account_drop.rb +4 -0
  14. data/lib/dugway/liquid/drops/features_drop.rb +144 -0
  15. data/lib/dugway/liquid/drops/product_drop.rb +8 -0
  16. data/lib/dugway/liquid/drops/products_drop.rb +1 -1
  17. data/lib/dugway/liquid/drops/related_products_drop.rb +88 -0
  18. data/lib/dugway/liquid/drops/theme_drop.rb +23 -0
  19. data/lib/dugway/liquid/drops/translations_drop.rb +122 -0
  20. data/lib/dugway/liquifier.rb +44 -8
  21. data/lib/dugway/store.rb +7 -2
  22. data/lib/dugway/theme.rb +169 -3
  23. data/lib/dugway/version.rb +1 -1
  24. data/lib/dugway.rb +31 -1
  25. data/locales/storefront.de.yml +79 -0
  26. data/locales/storefront.en-CA.yml +79 -0
  27. data/locales/storefront.en-GB.yml +79 -0
  28. data/locales/storefront.en-US.yml +79 -0
  29. data/locales/storefront.es-ES.yml +79 -0
  30. data/locales/storefront.es-MX.yml +79 -0
  31. data/locales/storefront.fr-CA.yml +79 -0
  32. data/locales/storefront.fr-FR.yml +79 -0
  33. data/locales/storefront.id.yml +79 -0
  34. data/locales/storefront.it.yml +79 -0
  35. data/locales/storefront.ja.yml +79 -0
  36. data/locales/storefront.ko.yml +79 -0
  37. data/locales/storefront.nl.yml +79 -0
  38. data/locales/storefront.pl.yml +79 -0
  39. data/locales/storefront.pt-BR.yml +79 -0
  40. data/locales/storefront.pt-PT.yml +79 -0
  41. data/locales/storefront.ro.yml +79 -0
  42. data/locales/storefront.sv.yml +79 -0
  43. data/locales/storefront.tr.yml +79 -0
  44. data/locales/storefront.zh-CN.yml +79 -0
  45. data/locales/storefront.zh-TW.yml +79 -0
  46. data/log/dugway.log +1 -0
  47. data/spec/features/page_rendering_spec.rb +4 -4
  48. data/spec/fixtures/theme/layout.html +2 -0
  49. data/spec/fixtures/theme/settings.json +6 -0
  50. data/spec/spec_helper.rb +4 -0
  51. data/spec/units/dugway/liquid/drops/features_drop_spec.rb +182 -0
  52. data/spec/units/dugway/liquid/drops/product_drop_spec.rb +36 -0
  53. data/spec/units/dugway/liquid/drops/related_products_drop_spec.rb +80 -0
  54. data/spec/units/dugway/liquid/drops/theme_drop_spec.rb +45 -0
  55. data/spec/units/dugway/liquid/drops/translations_drop_spec.rb +292 -0
  56. data/spec/units/dugway/store_spec.rb +37 -0
  57. data/spec/units/dugway/theme_spec.rb +456 -0
  58. metadata +35 -2
@@ -70,6 +70,8 @@
70
70
  {% endif %}
71
71
 
72
72
  <section>
73
+ {# Added for testing page.name variable #}
74
+ <span data-testid="page-name-test" style="display: none;">{{ page.name }}</span>
73
75
  {% if page.category == 'custom' %}
74
76
  <h1>{{ page.name }}</h1>
75
77
  {{ page_content | paragraphs }}
@@ -5,11 +5,13 @@
5
5
  {
6
6
  "variable": "logo",
7
7
  "label": "Logo",
8
+ "section": "global_navigation",
8
9
  "description": "Good for an image up to 150 pixels wide",
9
10
  "default": "logo_bc.png"
10
11
  },
11
12
  {
12
13
  "variable": "background_image",
14
+ "section": "general",
13
15
  "label": "Background Image",
14
16
  "description": "Adds a repeating background image"
15
17
  }
@@ -18,6 +20,7 @@
18
20
  {
19
21
  "variable": "slideshow_images",
20
22
  "label": "Slideshow",
23
+ "section": "homepage",
21
24
  "description": "This is a slideshow",
22
25
  "defaults": [
23
26
  "slideshow/1.gif",
@@ -30,6 +33,7 @@
30
33
  {
31
34
  "variable": "lookbook_images",
32
35
  "label": "Lookbook",
36
+ "section": "general",
33
37
  "description": "This is a lookbook"
34
38
  }
35
39
  ],
@@ -133,6 +137,7 @@
133
137
  {
134
138
  "variable": "show_search",
135
139
  "label": "Show search",
140
+ "section": "global_navigation",
136
141
  "type": "boolean",
137
142
  "default": false,
138
143
  "description": "Shows a search field"
@@ -140,6 +145,7 @@
140
145
  {
141
146
  "variable": "fixed_sidebar",
142
147
  "label": "Fixed Sidebar",
148
+ "section": "global_navigation",
143
149
  "type": "boolean",
144
150
  "default": true,
145
151
  "description": "Keeps the sidebar stationary while the page scrolls"
data/spec/spec_helper.rb CHANGED
@@ -10,6 +10,10 @@ RSpec.configure do |config|
10
10
  fixture_path = File.join(Dir.pwd, 'spec', 'fixtures')
11
11
 
12
12
  config.before(:each) do
13
+ # Stub Dugway.logger to write to /dev/null before each test
14
+ # This prevents log file creation/modification by the test suite.
15
+ allow(Dugway).to receive(:logger).and_return(Logger.new(File::NULL))
16
+
13
17
  # Stub api calls
14
18
  stub_request(:get, /.*api\.bigcartel\.com.*/).to_return(lambda { |request|
15
19
  { :body => File.new(File.join(fixture_path, 'store', request.uri.path.split('/', 3).last)), :status => 200, :headers => {} }
@@ -0,0 +1,182 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dugway::Drops::FeaturesDrop do
4
+ # Define the base source data representing the top-level 'features' block
5
+ # expected from the store configuration (e.g., dugway.json).
6
+ let(:feature_source_data) do
7
+ {
8
+ 'definitions' => {
9
+ 'feature_a' => 'enabled_by_default', # Will be tested with no override, and with opt-out
10
+ 'feature_b' => 'disabled_by_default' # Will be tested with no override, and with opt-in
11
+ },
12
+ # Opt-in 'feature_b' to test overriding disabled default
13
+ # Opt-in 'feature_a' to test conflict where opt-out wins
14
+ 'opt_ins' => ['feature_b', 'feature_a'],
15
+ # Opt-out 'feature_a' to test overriding enabled default and conflict
16
+ 'opt_outs' => ['feature_a']
17
+ }
18
+ end
19
+
20
+ # Initialize the drop with the source data
21
+ let(:features_drop) { described_class.new(feature_source_data) }
22
+
23
+ # Note: The 'before' block setting up theme_mock is removed as we now pass source directly.
24
+ # BaseDrop might still require context for other things (like request/params),
25
+ # but it's not needed for these specific tests focused on feature logic.
26
+
27
+ describe '#feature_definitions' do
28
+ it 'returns the definitions from the source hash' do
29
+ expect(features_drop.feature_definitions).to eq(feature_source_data['definitions'])
30
+ end
31
+
32
+ it 'returns empty hash if definitions are missing in source' do
33
+ drop = described_class.new(feature_source_data.except('definitions'))
34
+ expect(drop.feature_definitions).to eq({})
35
+ end
36
+
37
+ it 'returns empty hash if source is nil' do
38
+ drop = described_class.new(nil)
39
+ expect(drop.feature_definitions).to eq({})
40
+ end
41
+
42
+ it 'returns empty hash if source is empty' do
43
+ drop = described_class.new({})
44
+ expect(drop.feature_definitions).to eq({})
45
+ end
46
+ end
47
+
48
+ describe '#opt_ins' do
49
+ it 'returns filtered opt_ins from source' do
50
+ # Only includes opt-ins that are also in definitions
51
+ expect(features_drop.opt_ins).to match_array(['feature_a', 'feature_b'])
52
+ end
53
+
54
+ it 'returns empty array if opt_ins are missing in source' do
55
+ drop = described_class.new(feature_source_data.except('opt_ins'))
56
+ expect(drop.opt_ins).to eq([])
57
+ end
58
+
59
+ it 'returns empty array if definitions are missing in source' do
60
+ # Because opt_ins are filtered by defined features
61
+ drop = described_class.new(feature_source_data.except('definitions'))
62
+ expect(drop.opt_ins).to eq([])
63
+ end
64
+
65
+ it 'returns empty array if source is nil' do
66
+ drop = described_class.new(nil)
67
+ expect(drop.opt_ins).to eq([])
68
+ end
69
+ end
70
+
71
+ describe '#opt_outs' do
72
+ it 'returns filtered opt_outs from source' do
73
+ # Only includes opt-outs that are also in definitions
74
+ expect(features_drop.opt_outs).to match_array(['feature_a'])
75
+ end
76
+
77
+ it 'returns empty array if opt_outs are missing in source' do
78
+ drop = described_class.new(feature_source_data.except('opt_outs'))
79
+ expect(drop.opt_outs).to eq([])
80
+ end
81
+
82
+ it 'returns empty array if definitions are missing in source' do
83
+ # Because opt_outs are filtered by defined features
84
+ drop = described_class.new(feature_source_data.except('definitions'))
85
+ expect(drop.opt_outs).to eq([])
86
+ end
87
+
88
+ it 'returns empty array if source is nil' do
89
+ drop = described_class.new(nil)
90
+ expect(drop.opt_outs).to eq([])
91
+ end
92
+ end
93
+
94
+ describe 'dynamic feature check methods' do
95
+ context 'when feature is enabled by default (feature_a)' do
96
+ it 'returns true if not opted out' do
97
+ source_without_opt_out = feature_source_data.merge('opt_outs' => [])
98
+ drop = described_class.new(source_without_opt_out)
99
+ expect(drop.has_feature_a?).to be true
100
+ expect(drop.has_feature_a).to be true
101
+ end
102
+
103
+ it 'returns false if opted out' do
104
+ expect(features_drop.has_feature_a?).to be false
105
+ expect(features_drop.has_feature_a).to be false
106
+ end
107
+ end
108
+
109
+ context 'when feature is disabled by default (feature_b)' do
110
+ it 'returns false if not opted in' do
111
+ source_without_opt_in = feature_source_data.merge('opt_ins' => [])
112
+ drop = described_class.new(source_without_opt_in)
113
+ expect(drop.has_feature_b?).to be false
114
+ expect(drop.has_feature_b).to be false
115
+ end
116
+
117
+ it 'returns true if opted in' do
118
+ expect(features_drop.has_feature_b?).to be true
119
+ expect(features_drop.has_feature_b).to be true
120
+ end
121
+ end
122
+
123
+ context 'with conflicting opt-in/out (feature_a)' do
124
+ it 'returns false because opt-out takes precedence' do
125
+ expect(features_drop.has_feature_a?).to be false
126
+ expect(features_drop.has_feature_a).to be false
127
+ end
128
+ end
129
+
130
+ context 'when feature is not defined' do
131
+ it 'returns false' do
132
+ expect(features_drop.has_undefined_feature?).to be false
133
+ expect(features_drop.has_undefined_feature).to be false
134
+ end
135
+ end
136
+
137
+ context 'when source data is empty' do
138
+ let(:empty_source) { {} }
139
+ let(:drop_with_empty_source) { described_class.new(empty_source) }
140
+
141
+ it 'returns false for any feature check' do
142
+ expect(drop_with_empty_source.has_feature_a?).to be false
143
+ expect(drop_with_empty_source.has_any_other_feature).to be false
144
+ end
145
+ end
146
+
147
+ context 'when source data is nil' do
148
+ let(:nil_source) { nil }
149
+ let(:drop_with_nil_source) { described_class.new(nil_source) }
150
+
151
+ it 'returns false for any feature check' do
152
+ expect(drop_with_nil_source.has_feature_a?).to be false
153
+ expect(drop_with_nil_source.has_any_other_feature).to be false
154
+ end
155
+ end
156
+ end
157
+
158
+ describe '#respond_to?' do
159
+ # NOTE: Standard Ruby behavior means respond_to? is false for methods
160
+ # handled by method_missing unless respond_to_missing? is also defined.
161
+ it 'returns false for dynamic methods' do
162
+ expect(features_drop.respond_to?(:has_feature_a?)).to be false
163
+ expect(features_drop.respond_to?('has_feature_b?')).to be false
164
+ expect(features_drop.respond_to?(:has_feature_a)).to be false
165
+ expect(features_drop.respond_to?('has_feature_b')).to be false
166
+ end
167
+
168
+ it 'returns false for undefined dynamic methods' do
169
+ expect(features_drop.respond_to?(:has_undefined_feature?)).to be false
170
+ expect(features_drop.respond_to?(:has_undefined_feature)).to be false
171
+ end
172
+
173
+ it 'returns true for existing methods' do
174
+ expect(features_drop.respond_to?(:opt_ins)).to be true
175
+ expect(features_drop.respond_to?(:source)).to be true # From BaseDrop initialization
176
+ end
177
+
178
+ it 'returns false for completely unknown methods' do
179
+ expect(features_drop.respond_to?(:some_random_method)).to be false
180
+ end
181
+ end
182
+ end
@@ -3,6 +3,19 @@ require 'spec_helper'
3
3
  describe Dugway::Drops::ProductDrop do
4
4
  let(:product) { Dugway::Drops::ProductDrop.new(Dugway.store.products.first) }
5
5
 
6
+ let(:related_products_mock) do
7
+ [
8
+ { 'id' => 2, 'name' => 'Related Product 1' },
9
+ { 'id' => 3, 'name' => 'Related Product 2' }
10
+ ]
11
+ end
12
+
13
+ before do
14
+ allow(Dugway::Drops::RelatedProductsDrop).to receive(:new)
15
+ .with(product.source, limit: 5, sort_order: nil)
16
+ .and_return(double(products: related_products_mock))
17
+ end
18
+
6
19
  describe "#id" do
7
20
  it "should return the product's id" do
8
21
  product.id.should == 9422939
@@ -266,4 +279,27 @@ describe Dugway::Drops::ProductDrop do
266
279
  end
267
280
  end
268
281
  end
282
+
283
+ describe "#related_products" do
284
+ let(:theme) { double('Dugway::Theme') }
285
+ let(:theme_customization) do
286
+ {
287
+ 'related_items' => 5,
288
+ 'related_products_order' => 'position'
289
+ }
290
+ end
291
+
292
+ before do
293
+ allow(Dugway).to receive(:theme).and_return(theme)
294
+ allow(theme).to receive(:customization).and_return(theme_customization)
295
+ allow(Dugway::Drops::RelatedProductsDrop).to receive(:new)
296
+ .with(product.source)
297
+ .and_return(double(products: related_products_mock))
298
+ end
299
+
300
+ it "returns the related products from RelatedProductsDrop" do
301
+ related_products = product.related_products
302
+ expect(related_products).to eq(related_products_mock)
303
+ end
304
+ end
269
305
  end
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dugway::Drops::RelatedProductsDrop do
4
+ let(:product) do
5
+ {
6
+ 'id' => 1,
7
+ 'category_ids' => [1, 2],
8
+ 'name' => 'Sample Product'
9
+ }
10
+ end
11
+
12
+ let(:related_product_1) do
13
+ {
14
+ 'id' => 2,
15
+ 'category_ids' => [1],
16
+ 'name' => 'Related Product 1'
17
+ }
18
+ end
19
+
20
+ let(:related_product_2) do
21
+ {
22
+ 'id' => 3,
23
+ 'category_ids' => [2],
24
+ 'name' => 'Related Product 2'
25
+ }
26
+ end
27
+
28
+ let(:unrelated_product) do
29
+ {
30
+ 'id' => 4,
31
+ 'category_ids' => [3],
32
+ 'name' => 'Unrelated Product'
33
+ }
34
+ end
35
+
36
+ let(:theme) { double('Dugway::Theme') }
37
+ let(:theme_customization) do
38
+ {
39
+ 'related_items' => 2,
40
+ 'related_products_order' => 'position'
41
+ }
42
+ end
43
+
44
+ before do
45
+ allow(Dugway.store).to receive(:products).and_return([
46
+ product,
47
+ related_product_1,
48
+ related_product_2,
49
+ unrelated_product
50
+ ])
51
+ allow(Dugway).to receive(:theme).and_return(theme)
52
+ allow(theme).to receive(:customization).and_return(theme_customization)
53
+ end
54
+
55
+ let(:drop) { described_class.new(product) }
56
+
57
+ describe "#products" do
58
+ it "returns related products within the same categories" do
59
+ products = drop.products
60
+ expect(products.map { |p| p['id'] }).to match_array([2, 3])
61
+ end
62
+
63
+ it "limits the number of related products returned" do
64
+ allow(theme).to receive(:customization).and_return({ 'related_items' => 1 })
65
+ products = drop.products
66
+ expect(products.size).to eq(1)
67
+ end
68
+
69
+ it "excludes the original product from the results" do
70
+ products = drop.products
71
+ expect(products.map { |p| p['id'] }).not_to include(product['id'])
72
+ end
73
+
74
+ it "falls back to other products if category matches are insufficient" do
75
+ product['category_ids'] = [] # No categories
76
+ products = drop.products
77
+ expect(products.map { |p| p['id'] }).to match_array([2, 3])
78
+ end
79
+ end
80
+ end
@@ -127,4 +127,49 @@ describe Dugway::Drops::ThemeDrop do
127
127
  end
128
128
  end
129
129
  end
130
+
131
+ describe '#features' do
132
+ let(:mock_features_config) do
133
+ {
134
+ 'definitions' => { 'feat_a' => 'enabled_by_default' },
135
+ 'opt_ins' => [],
136
+ 'opt_outs' => ['feat_a']
137
+ }
138
+ end
139
+ let(:mock_options) { { 'features' => mock_features_config } }
140
+
141
+ before do
142
+ # Stub the global Dugway.options for these tests
143
+ allow(Dugway).to receive(:options).and_return(mock_options)
144
+ # ThemeDrop needs context to potentially access registers, even if not used by #features itself
145
+ theme.context = context
146
+ end
147
+
148
+ it 'initializes FeaturesDrop with data from Dugway.options["features"]' do
149
+ features_drop_instance = theme.features # Call the method under test
150
+
151
+ expect(features_drop_instance).to be_an_instance_of(Dugway::Drops::FeaturesDrop)
152
+ # Verify that the source passed to FeaturesDrop was correct
153
+ expect(features_drop_instance.instance_variable_get(:@source)).to eq(mock_features_config)
154
+ # Optionally, check a derived value to ensure data was processed
155
+ expect(features_drop_instance.feature_definitions).to eq(mock_features_config['definitions'])
156
+ expect(features_drop_instance.opt_outs).to eq(['feat_a']) # Check filtering based on definitions
157
+ end
158
+
159
+ it 'initializes FeaturesDrop with empty hash if features key is missing in options' do
160
+ allow(Dugway).to receive(:options).and_return({}) # No 'features' key
161
+ features_drop_instance = theme.features
162
+ expect(features_drop_instance).to be_an_instance_of(Dugway::Drops::FeaturesDrop)
163
+ expect(features_drop_instance.instance_variable_get(:@source)).to eq({})
164
+ expect(features_drop_instance.feature_definitions).to eq({})
165
+ end
166
+
167
+ it 'initializes FeaturesDrop with empty hash if Dugway.options is nil' do
168
+ allow(Dugway).to receive(:options).and_return(nil)
169
+ features_drop_instance = theme.features
170
+ expect(features_drop_instance).to be_an_instance_of(Dugway::Drops::FeaturesDrop)
171
+ expect(features_drop_instance.instance_variable_get(:@source)).to eq({})
172
+ expect(features_drop_instance.feature_definitions).to eq({})
173
+ end
174
+ end
130
175
  end