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.
- checksums.yaml +4 -4
- data/.bundle/config +2 -0
- data/.github/workflows/main.yml +1 -1
- data/.gitignore +1 -0
- data/README.md +9 -0
- data/lib/dugway/application.rb +5 -3
- data/lib/dugway/assets/big_cartel_logo.svg +4 -0
- data/lib/dugway/cli/build.rb +18 -1
- data/lib/dugway/cli/server.rb +2 -2
- data/lib/dugway/cli/templates/source/settings.json +8 -0
- data/lib/dugway/cli/validate.rb +20 -2
- data/lib/dugway/controller.rb +5 -1
- data/lib/dugway/liquid/drops/account_drop.rb +4 -0
- data/lib/dugway/liquid/drops/features_drop.rb +144 -0
- data/lib/dugway/liquid/drops/product_drop.rb +8 -0
- data/lib/dugway/liquid/drops/products_drop.rb +1 -1
- data/lib/dugway/liquid/drops/related_products_drop.rb +88 -0
- data/lib/dugway/liquid/drops/theme_drop.rb +23 -0
- data/lib/dugway/liquid/drops/translations_drop.rb +122 -0
- data/lib/dugway/liquifier.rb +44 -8
- data/lib/dugway/store.rb +7 -2
- data/lib/dugway/theme.rb +169 -3
- data/lib/dugway/version.rb +1 -1
- data/lib/dugway.rb +31 -1
- data/locales/storefront.de.yml +79 -0
- data/locales/storefront.en-CA.yml +79 -0
- data/locales/storefront.en-GB.yml +79 -0
- data/locales/storefront.en-US.yml +79 -0
- data/locales/storefront.es-ES.yml +79 -0
- data/locales/storefront.es-MX.yml +79 -0
- data/locales/storefront.fr-CA.yml +79 -0
- data/locales/storefront.fr-FR.yml +79 -0
- data/locales/storefront.id.yml +79 -0
- data/locales/storefront.it.yml +79 -0
- data/locales/storefront.ja.yml +79 -0
- data/locales/storefront.ko.yml +79 -0
- data/locales/storefront.nl.yml +79 -0
- data/locales/storefront.pl.yml +79 -0
- data/locales/storefront.pt-BR.yml +79 -0
- data/locales/storefront.pt-PT.yml +79 -0
- data/locales/storefront.ro.yml +79 -0
- data/locales/storefront.sv.yml +79 -0
- data/locales/storefront.tr.yml +79 -0
- data/locales/storefront.zh-CN.yml +79 -0
- data/locales/storefront.zh-TW.yml +79 -0
- data/log/dugway.log +1 -0
- data/spec/features/page_rendering_spec.rb +4 -4
- data/spec/fixtures/theme/layout.html +2 -0
- data/spec/fixtures/theme/settings.json +6 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/units/dugway/liquid/drops/features_drop_spec.rb +182 -0
- data/spec/units/dugway/liquid/drops/product_drop_spec.rb +36 -0
- data/spec/units/dugway/liquid/drops/related_products_drop_spec.rb +80 -0
- data/spec/units/dugway/liquid/drops/theme_drop_spec.rb +45 -0
- data/spec/units/dugway/liquid/drops/translations_drop_spec.rb +292 -0
- data/spec/units/dugway/store_spec.rb +37 -0
- data/spec/units/dugway/theme_spec.rb +456 -0
- 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
|