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,81 @@
1
+ zh-TW:
2
+ storefront:
3
+ navigation:
4
+ all: "全部"
5
+ all_products: "所有商品"
6
+ back_to_site: "返回網站"
7
+ cart: "購物車"
8
+ categories: "分類"
9
+ contact: "聯絡我們"
10
+ home: "首頁"
11
+ item: "件商品"
12
+ items: "件商品"
13
+ more: "更多"
14
+ next: "下一頁"
15
+ newest: "最新"
16
+ pages: "頁面"
17
+ previous: "上一頁"
18
+ products: "商品"
19
+ quick_view: "快速預覽"
20
+ search: "搜尋"
21
+ shop: "商店"
22
+ social: "关注我们"
23
+ subscribe: "訂閱"
24
+ top_selling: "熱銷"
25
+ view: "查看"
26
+ view_all: "查看全部"
27
+ # "Sort By" translations are used in very few themes, and are not exposed in theme settings for sellers to configure
28
+ sort_by: 排序
29
+ sort_by_featured: 精選商品
30
+ sort_by_on_sale: 特價商品
31
+ sort_by_top_selling: 熱銷商品
32
+ sort_by_alphabetically_a_to_z: 英文字母 (A 到 Z)
33
+ sort_by_alphabetically_z_to_a: 英文字母 (Z 到 A)
34
+ sort_by_date_new_to_old: 日期 (最新到最舊)
35
+ sort_by_date_old_to_new: 日期 (最舊到最新)
36
+ sort_by_price_low_to_high: 價格 (低到高)
37
+ sort_by_price_high_to_low: 價格 (高到低)
38
+ home:
39
+ all_products: "所有商品"
40
+ featured: "精選"
41
+ featured_categories: "精選分類"
42
+ featured_products: "精選商品"
43
+ featured_video: ""
44
+ products:
45
+ add_to_cart: "加入購物車"
46
+ added: "已加入" # Used in few themes, intentionally not exposed in theme settings
47
+ adding: "加入中" # Used in few themes, intentionally not exposed in theme settings
48
+ almost_sold_out: "即將售罄!"
49
+ coming_soon: "即將上線"
50
+ description: "商品描述"
51
+ in_stock: "有庫存"
52
+ inventory: "庫存"
53
+ low_inventory: "庫存有限"
54
+ no_products: "未找到商品"
55
+ on_sale: "特價"
56
+ related_products: "相關商品"
57
+ reset: "重設"
58
+ search_results: "搜尋結果"
59
+ select: "選擇"
60
+ select_variant: "選擇規格"
61
+ sold_out: "已售罄"
62
+ variant: "規格"
63
+ variants: "規格"
64
+ cart:
65
+ checkout: "結帳"
66
+ continue_shopping: "繼續購物"
67
+ empty_cart: "您的購物車是空的"
68
+ quantity_abbreviated: "数量"
69
+ quantity: "数量"
70
+ remove: "移除"
71
+ share_this_cart: "分享購物車"
72
+ share_this_cart_link_copy_success: "連結已複製!"
73
+ subtotal: "小計"
74
+ view_cart: "查看購物車"
75
+ contact:
76
+ email: "電子郵件"
77
+ form_success: "謝謝!您的訊息已發送,我們將盡快回覆您。"
78
+ message: "留言"
79
+ name: "姓名"
80
+ send_button: "發送留言"
81
+ subject: "主題"
data/log/dugway.log ADDED
@@ -0,0 +1 @@
1
+ # Logfile created on 2025-04-08 18:41:37 -0700 by logger.rb/v1.4.2
data/mise.toml ADDED
@@ -0,0 +1,2 @@
1
+ [tools]
2
+ ruby = "2.7.8"
@@ -11,7 +11,7 @@ feature 'Page rendering' do
11
11
  scenario 'products.html' do
12
12
  visit '/products'
13
13
  expect(page).to have_content('Dugway') # layout.html
14
- expect(page).to have_content('Products')
14
+ expect(page).to have_selector('[data-testid="page-name-test"]', text: 'Products', visible: :all)
15
15
  expect(page).to have_content('My Product')
16
16
  expect(page).to have_content('$10.00')
17
17
  end
@@ -19,7 +19,7 @@ feature 'Page rendering' do
19
19
  scenario 'products.html via artist' do
20
20
  visit '/artist/artist-one'
21
21
  expect(page).to have_content('Dugway') # layout.html
22
- expect(page).to have_content('Artist One')
22
+ expect(page).to have_selector('[data-testid="page-name-test"]', text: 'Artist One', visible: :all)
23
23
  expect(page).to have_content('My Product')
24
24
  expect(page).to have_content('$10.00')
25
25
  end
@@ -27,7 +27,7 @@ feature 'Page rendering' do
27
27
  scenario 'products.html via category' do
28
28
  visit '/category/tees'
29
29
  expect(page).to have_content('Dugway') # layout.html
30
- expect(page).to have_content('Tees')
30
+ expect(page).to have_selector('[data-testid="page-name-test"]', text: 'Tees', visible: :all)
31
31
  expect(page).to have_content('My Product')
32
32
  expect(page).to have_content('$10.00')
33
33
  end
@@ -35,7 +35,7 @@ feature 'Page rendering' do
35
35
  scenario 'product.html' do
36
36
  visit '/product/my-product'
37
37
  expect(page).to have_content('Dugway') # layout.html
38
- expect(page).to have_content('My Product')
38
+ expect(page).to have_selector('[data-testid="page-name-test"]', text: 'My Product', visible: :all)
39
39
  expect(page).to have_content('$10.00')
40
40
  end
41
41
 
@@ -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,13 @@ RSpec.configure do |config|
10
10
  fixture_path = File.join(Dir.pwd, 'spec', 'fixtures')
11
11
 
12
12
  config.before(:each) do
13
+ # Reset cart state between tests
14
+ Dugway.instance_variable_set(:@cart, nil)
15
+
16
+ # Stub Dugway.logger to write to /dev/null before each test
17
+ # This prevents log file creation/modification by the test suite.
18
+ allow(Dugway).to receive(:logger).and_return(Logger.new(File::NULL))
19
+
13
20
  # Stub api calls
14
21
  stub_request(:get, /.*api\.bigcartel\.com.*/).to_return(lambda { |request|
15
22
  { :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
@@ -1,6 +1,12 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Dugway::Drops::PagesDrop do
4
+ before(:each) do
5
+ # Reset store state before each test
6
+ Dugway.store.instance_variable_set(:@pages, nil)
7
+ Dugway.store.instance_variable_set(:@external_pages, nil)
8
+ end
9
+
4
10
  let(:pages) do
5
11
  Dugway::Drops::PagesDrop.new(
6
12
  Dugway.store.pages.map do |p|
@@ -14,15 +20,58 @@ describe Dugway::Drops::PagesDrop do
14
20
  )
15
21
  end
16
22
 
23
+ def create_pages_with_external(external_pages_data)
24
+ # Mock the store before creating pages
25
+ allow(Dugway.store).to receive(:external_pages).and_return(external_pages_data)
26
+ # Clear memoized pages
27
+ Dugway.store.instance_variable_set(:@pages, nil)
28
+
29
+ Dugway::Drops::PagesDrop.new(
30
+ Dugway.store.pages.map do |p|
31
+ case p["permalink"]
32
+ when "cart"
33
+ Dugway::Drops::CartDrop.new(p)
34
+ else
35
+ Dugway::Drops::PageDrop.new(p)
36
+ end
37
+ end
38
+ )
39
+ end
40
+
17
41
  describe "#all" do
18
- it "should return an array of all pages" do
19
- all = pages.all
20
- all.should be_an_instance_of(Array)
21
- all.size.should == 1
42
+ context "with only custom pages" do
43
+ it "should return an array of all custom pages" do
44
+ all = pages.all
45
+ all.should be_an_instance_of(Array)
46
+ all.size.should == 1
22
47
 
23
- page = all.first
24
- page.should be_an_instance_of(Dugway::Drops::PageDrop)
25
- page.name.should == 'About Us'
48
+ page = all.first
49
+ page.should be_an_instance_of(Dugway::Drops::PageDrop)
50
+ page.name.should == 'About Us'
51
+ end
52
+ end
53
+
54
+ context "with custom and external pages" do
55
+ it "should return an array of all pages (custom + external)" do
56
+ pages_with_external = create_pages_with_external([
57
+ {
58
+ 'name' => 'Subscribe',
59
+ 'permalink' => 'subscribe',
60
+ 'url' => 'https://example.com/subscribe',
61
+ 'category' => 'external'
62
+ }
63
+ ])
64
+
65
+ all = pages_with_external.all
66
+ all.should be_an_instance_of(Array)
67
+ all.size.should == 2
68
+
69
+ custom_page = all.find { |p| p.name == 'About Us' }
70
+ custom_page.should be_an_instance_of(Dugway::Drops::PageDrop)
71
+
72
+ external_page = all.find { |p| p.name == 'Subscribe' }
73
+ external_page.should be_an_instance_of(Dugway::Drops::PageDrop)
74
+ end
26
75
  end
27
76
  end
28
77
 
@@ -46,6 +95,136 @@ describe Dugway::Drops::PagesDrop do
46
95
  end
47
96
  end
48
97
 
98
+ describe "#subscribe_page" do
99
+ context "when subscribe_url is configured" do
100
+ it "should return the subscribe page" do
101
+ allow(Dugway.store).to receive(:subscribe_url).and_return('https://example.com/subscribe')
102
+ pages_with_subscribe = create_pages_with_external([
103
+ {
104
+ 'name' => 'Subscribe',
105
+ 'permalink' => 'subscribe',
106
+ 'url' => 'https://example.com/subscribe',
107
+ 'category' => 'external'
108
+ }
109
+ ])
110
+
111
+ subscribe_page = pages_with_subscribe.subscribe_page
112
+ subscribe_page.should be_an_instance_of(Dugway::Drops::PageDrop)
113
+ subscribe_page.name.should == 'Subscribe'
114
+ subscribe_page.url.should == 'https://example.com/subscribe'
115
+ end
116
+ end
117
+
118
+ context "when subscribe_url is not configured" do
119
+ it "should return nil" do
120
+ allow(Dugway.store).to receive(:subscribe_url).and_return(nil)
121
+ pages_without_subscribe = create_pages_with_external([])
122
+
123
+ pages_without_subscribe.subscribe_page.should be_nil
124
+ end
125
+ end
126
+ end
127
+
128
+ describe "#custom_pages" do
129
+ it "should return an array of custom pages" do
130
+ custom_pages = pages.custom_pages
131
+ custom_pages.should be_an_instance_of(Array)
132
+ custom_pages.size.should == 1
133
+ custom_pages.first.should be_an_instance_of(Dugway::Drops::PageDrop)
134
+ custom_pages.first.name.should == 'About Us'
135
+ end
136
+ end
137
+
138
+ describe "#external_pages" do
139
+ context "when subscribe_url is configured" do
140
+ it "should return an array with the subscribe page" do
141
+ pages_with_subscribe = create_pages_with_external([
142
+ {
143
+ 'name' => 'Subscribe',
144
+ 'permalink' => 'subscribe',
145
+ 'url' => 'https://example.com/subscribe',
146
+ 'category' => 'external'
147
+ }
148
+ ])
149
+
150
+ external_pages = pages_with_subscribe.external_pages
151
+ external_pages.should be_an_instance_of(Array)
152
+ external_pages.size.should == 1
153
+ external_pages.first.should be_an_instance_of(Dugway::Drops::PageDrop)
154
+ external_pages.first.name.should == 'Subscribe'
155
+ end
156
+ end
157
+
158
+ context "when subscribe_url is not configured" do
159
+ it "should return an empty array" do
160
+ pages_without_subscribe = create_pages_with_external([])
161
+
162
+ pages_without_subscribe.external_pages.should == []
163
+ end
164
+ end
165
+ end
166
+
167
+ describe "#required_pages" do
168
+ def create_pages_with_required(required_pages_data)
169
+ # Mock the store before creating pages
170
+ allow(Dugway.store).to receive(:required_pages).and_return(required_pages_data)
171
+ # Clear memoized pages
172
+ Dugway.store.instance_variable_set(:@pages, nil)
173
+
174
+ Dugway::Drops::PagesDrop.new(
175
+ Dugway.store.pages.map do |p|
176
+ case p["permalink"]
177
+ when "cart"
178
+ Dugway::Drops::CartDrop.new(p)
179
+ else
180
+ Dugway::Drops::PageDrop.new(p)
181
+ end
182
+ end
183
+ )
184
+ end
185
+
186
+ context "when required pages are configured" do
187
+ it "should return an array with the required pages" do
188
+ pages_with_required = create_pages_with_required([
189
+ {
190
+ 'name' => 'Terms of Service',
191
+ 'permalink' => 'terms-of-service',
192
+ 'url' => 'https://example.com/terms',
193
+ 'category' => 'custom',
194
+ 'required' => true
195
+ },
196
+ {
197
+ 'name' => 'Privacy Policy',
198
+ 'permalink' => 'privacy-policy',
199
+ 'url' => 'https://example.com/privacy',
200
+ 'category' => 'custom',
201
+ 'required' => true
202
+ }
203
+ ])
204
+
205
+ required_pages = pages_with_required.required_pages
206
+ required_pages.should be_an_instance_of(Array)
207
+ required_pages.size.should == 2
208
+
209
+ terms_page = required_pages.find { |p| p.name == 'Terms of Service' }
210
+ terms_page.should be_an_instance_of(Dugway::Drops::PageDrop)
211
+ terms_page.url.should == 'https://example.com/terms'
212
+
213
+ privacy_page = required_pages.find { |p| p.name == 'Privacy Policy' }
214
+ privacy_page.should be_an_instance_of(Dugway::Drops::PageDrop)
215
+ privacy_page.url.should == 'https://example.com/privacy'
216
+ end
217
+ end
218
+
219
+ context "when no required pages are configured" do
220
+ it "should return an empty array" do
221
+ pages_without_required = create_pages_with_required([])
222
+
223
+ pages_without_required.required_pages.should == []
224
+ end
225
+ end
226
+ end
227
+
49
228
  private
50
229
 
51
230
  def rendered_template(template, assigns={}, registers={})
@@ -302,4 +302,21 @@ describe Dugway::Drops::ProductDrop do
302
302
  expect(related_products).to eq(related_products_mock)
303
303
  end
304
304
  end
305
+
306
+ describe "#price_suffix" do
307
+ it "should return nil when no price_suffix is configured" do
308
+ allow(Dugway.store).to receive(:price_suffix).and_return(nil)
309
+ product.price_suffix.should be_nil
310
+ end
311
+
312
+ it "should return the price_suffix from store configuration" do
313
+ allow(Dugway.store).to receive(:price_suffix).and_return(' USD')
314
+ product.price_suffix.should == ' USD'
315
+ end
316
+
317
+ it "should return the price_suffix from store options" do
318
+ allow(Dugway.store).to receive(:price_suffix).and_return('+ HI')
319
+ product.price_suffix.should == '+ HI'
320
+ end
321
+ end
305
322
  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