dugway 1.1.0 → 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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.bundle/config +2 -0
  3. data/.gitignore +1 -0
  4. data/lib/dugway/application.rb +5 -3
  5. data/lib/dugway/assets/big_cartel_logo.svg +4 -0
  6. data/lib/dugway/cli/build.rb +7 -1
  7. data/lib/dugway/cli/server.rb +2 -2
  8. data/lib/dugway/cli/templates/source/settings.json +8 -0
  9. data/lib/dugway/cli/validate.rb +9 -2
  10. data/lib/dugway/controller.rb +5 -1
  11. data/lib/dugway/liquid/drops/account_drop.rb +4 -0
  12. data/lib/dugway/liquid/drops/features_drop.rb +144 -0
  13. data/lib/dugway/liquid/drops/theme_drop.rb +23 -0
  14. data/lib/dugway/liquid/drops/translations_drop.rb +122 -0
  15. data/lib/dugway/liquifier.rb +44 -8
  16. data/lib/dugway/store.rb +7 -2
  17. data/lib/dugway/theme.rb +107 -10
  18. data/lib/dugway/version.rb +1 -1
  19. data/lib/dugway.rb +31 -1
  20. data/locales/storefront.de.yml +79 -0
  21. data/locales/storefront.en-CA.yml +79 -0
  22. data/locales/storefront.en-GB.yml +79 -0
  23. data/locales/storefront.en-US.yml +79 -0
  24. data/locales/storefront.es-ES.yml +79 -0
  25. data/locales/storefront.es-MX.yml +79 -0
  26. data/locales/storefront.fr-CA.yml +79 -0
  27. data/locales/storefront.fr-FR.yml +79 -0
  28. data/locales/storefront.id.yml +79 -0
  29. data/locales/storefront.it.yml +79 -0
  30. data/locales/storefront.ja.yml +79 -0
  31. data/locales/storefront.ko.yml +79 -0
  32. data/locales/storefront.nl.yml +79 -0
  33. data/locales/storefront.pl.yml +79 -0
  34. data/locales/storefront.pt-BR.yml +79 -0
  35. data/locales/storefront.pt-PT.yml +79 -0
  36. data/locales/storefront.ro.yml +79 -0
  37. data/locales/storefront.sv.yml +79 -0
  38. data/locales/storefront.tr.yml +79 -0
  39. data/locales/storefront.zh-CN.yml +79 -0
  40. data/locales/storefront.zh-TW.yml +79 -0
  41. data/log/dugway.log +1 -0
  42. data/spec/features/page_rendering_spec.rb +4 -4
  43. data/spec/fixtures/theme/layout.html +2 -0
  44. data/spec/fixtures/theme/settings.json +6 -0
  45. data/spec/spec_helper.rb +4 -0
  46. data/spec/units/dugway/liquid/drops/features_drop_spec.rb +182 -0
  47. data/spec/units/dugway/liquid/drops/theme_drop_spec.rb +45 -0
  48. data/spec/units/dugway/liquid/drops/translations_drop_spec.rb +292 -0
  49. data/spec/units/dugway/store_spec.rb +37 -0
  50. data/spec/units/dugway/theme_spec.rb +297 -1
  51. metadata +32 -2
@@ -0,0 +1,79 @@
1
+ tr:
2
+ storefront:
3
+ navigation:
4
+ all: "Tümü"
5
+ all_products: "Tüm ürünler"
6
+ back_to_site: "Siteye dön"
7
+ cart: "Sepet"
8
+ categories: "Kategoriler"
9
+ contact: "İletişim"
10
+ home: "Ana sayfa"
11
+ item: "ürün"
12
+ items: "ürün"
13
+ more: "Daha fazla"
14
+ next: "Sonraki"
15
+ newest: "En yeni"
16
+ pages: "Sayfalar"
17
+ previous: "Önceki"
18
+ products: "Ürünler"
19
+ quick_view: "Hızlı görünüm"
20
+ search: "Ara"
21
+ shop: "Mağaza"
22
+ social: "Bizi takip edin"
23
+ subscribe: "Abone Ol"
24
+ top_selling: "Çok satanlar"
25
+ view: "Görüntüle"
26
+ view_all: "Tümünü görüntüle"
27
+ # "Sort By" translations are used in very few themes, and are not exposed in theme settings for sellers to configure
28
+ sort_by: Sırala
29
+ sort_by_featured: Öne Çıkanlar
30
+ sort_by_on_sale: İndirimde
31
+ sort_by_top_selling: En Çok Satanlar
32
+ sort_by_alphabetically_a_to_z: Alfabetik (A'dan Z'ye)
33
+ sort_by_alphabetically_z_to_a: Alfabetik (Z'den A'ya)
34
+ sort_by_date_new_to_old: Tarih (Yeniden Eskiye)
35
+ sort_by_date_old_to_new: Tarih (Eskiden Yeniye)
36
+ sort_by_price_low_to_high: Fiyat (Düşükten Yükseğe)
37
+ sort_by_price_high_to_low: Fiyat (Yüksekten Düşüğe)
38
+ home:
39
+ all_products: "Tüm ürünler"
40
+ featured: "Öne çıkan"
41
+ featured_categories: "Öne çıkan kategoriler"
42
+ featured_products: "Öne çıkan ürünler"
43
+ featured_video: ""
44
+ products:
45
+ add_to_cart: "Sepete ekle"
46
+ added: "Eklendi" # Used in few themes, intentionally not exposed in theme settings
47
+ adding: "Ekleniyor..." # Used in few themes, intentionally not exposed in theme settings
48
+ almost_sold_out: "Sadece birkaç tane kaldı!"
49
+ coming_soon: "Yakında"
50
+ description: "Açıklama'"
51
+ in_stock: "stokta"
52
+ inventory: "Stok Durumu"
53
+ low_inventory: "Sınırlı sayıda"
54
+ no_products: "Ürün bulunamadı"
55
+ on_sale: "İndirimde"
56
+ related_products: "İlgili ürünler"
57
+ reset: "Sıfırla"
58
+ search_results: "Arama sonuçları"
59
+ select: "Seç"
60
+ select_variant: "Seçenek seç"
61
+ sold_out: "Tükendi"
62
+ cart:
63
+ checkout: "Ödeme"
64
+ continue_shopping: "Alışverişe devam et"
65
+ empty_cart: "Sepetiniz boş"
66
+ quantity_abbreviated: "Ad."
67
+ quantity: "Adet"
68
+ remove: "Kaldır"
69
+ share_this_cart: "Bu sepeti paylaş"
70
+ share_this_cart_link_copy_success: "Bağlantı kopyalandı!"
71
+ subtotal: "Ara toplam"
72
+ view_cart: "Sepeti görüntüle"
73
+ contact:
74
+ email: "E-posta"
75
+ form_success: "Teşekkürler! Mesajınız gönderildi ve en kısa sürede size geri döneceğiz."
76
+ message: "Mesaj"
77
+ name: "İsim"
78
+ send_button: "Mesaj gönder"
79
+ subject: "Konu"
@@ -0,0 +1,79 @@
1
+ zh-CN:
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
+ cart:
63
+ checkout: "结账"
64
+ continue_shopping: "继续购物"
65
+ empty_cart: "您的购物车是空的"
66
+ quantity_abbreviated: "数量"
67
+ quantity: "数量"
68
+ remove: "移除"
69
+ share_this_cart: "分享购物车"
70
+ share_this_cart_link_copy_success: "链接已复制!"
71
+ subtotal: "小计"
72
+ view_cart: "查看购物车"
73
+ contact:
74
+ email: "电子邮箱"
75
+ form_success: "谢谢!您的消息已发送,我们将尽快回复您。"
76
+ message: "留言"
77
+ name: "姓名"
78
+ send_button: "发送留言"
79
+ subject: "主题"
@@ -0,0 +1,79 @@
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
+ cart:
63
+ checkout: "結帳"
64
+ continue_shopping: "繼續購物"
65
+ empty_cart: "您的購物車是空的"
66
+ quantity_abbreviated: "数量"
67
+ quantity: "数量"
68
+ remove: "移除"
69
+ share_this_cart: "分享購物車"
70
+ share_this_cart_link_copy_success: "連結已複製!"
71
+ subtotal: "小計"
72
+ view_cart: "查看購物車"
73
+ contact:
74
+ email: "電子郵件"
75
+ form_success: "謝謝!您的訊息已發送,我們將盡快回覆您。"
76
+ message: "留言"
77
+ name: "姓名"
78
+ send_button: "發送留言"
79
+ 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
@@ -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,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
@@ -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