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.
- checksums.yaml +4 -4
- data/.bundle/config +2 -0
- data/.gitignore +1 -0
- data/lib/dugway/application.rb +5 -3
- data/lib/dugway/assets/big_cartel_logo.svg +4 -0
- data/lib/dugway/cli/build.rb +7 -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 +9 -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/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 +107 -10
- 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/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 +297 -1
- 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
|
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
|
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
|
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
|
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
|