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
@@ -189,4 +189,59 @@ describe Dugway::Store do
189
189
  store.locale.should == 'en-US'
190
190
  end
191
191
  end
192
+
193
+ describe "#website" do
194
+ context "when website is provided in store_options" do
195
+ let(:store_options) { { website: 'https://my-custom-override.com' } }
196
+ let(:store) { Dugway::Store.new('dugway', store_options) }
197
+
198
+ it "returns the website from store_options" do
199
+ store.website.should == 'https://my-custom-override.com'
200
+ end
201
+ end
202
+
203
+ context "when website is not in store_options but in account data" do
204
+ let(:store) { Dugway::Store.new('dugway', {}) } # No override
205
+
206
+ before do
207
+ # Mock the account data to include a website
208
+ allow(store).to receive(:account).and_return({ 'website' => 'https://api-provided-site.com' })
209
+ end
210
+
211
+ it "returns the website from account data" do
212
+ store.website.should == 'https://api-provided-site.com'
213
+ end
214
+ end
215
+
216
+ context "when website is in neither store_options nor account data" do
217
+ let(:store) { Dugway::Store.new('dugway', {}) } # No override
218
+
219
+ before do
220
+ # Mock the account data to NOT include a website
221
+ allow(store).to receive(:account).and_return({})
222
+ end
223
+
224
+ it "returns nil" do
225
+ store.website.should be_nil
226
+ end
227
+ end
228
+ end
229
+
230
+ describe "#price_suffix" do
231
+ context "when price_suffix is in store_options" do
232
+ let(:store) { Dugway::Store.new('dugway', { price_suffix: '+ HI' }) }
233
+
234
+ it "returns the price_suffix from store_options" do
235
+ store.price_suffix.should == '+ HI'
236
+ end
237
+ end
238
+
239
+ context "when price_suffix is not in store_options" do
240
+ let(:store) { Dugway::Store.new('dugway', {}) }
241
+
242
+ it "returns nil" do
243
+ store.price_suffix.should be_nil
244
+ end
245
+ end
246
+ end
192
247
  end
@@ -387,7 +387,7 @@ describe Dugway::Theme do
387
387
  {'variable' => 'button_text_color'},
388
388
  {'variable' => 'button_hover_background_color'}
389
389
  ],
390
- 'options' => [{ 'variable' => 'show_search', 'label' => 'Show search' }]
390
+ 'options' => [{ 'variable' => 'show_search', 'section' => 'global_navigation', 'label' => 'Show search' }]
391
391
  } }
392
392
  end
393
393
 
@@ -417,6 +417,7 @@ describe Dugway::Theme do
417
417
  settings = theme.settings.merge({
418
418
  'options' => [{
419
419
  'variable' => 'show_search',
420
+ 'section' => 'global_navigation',
420
421
  'label' => 'Show search',
421
422
  'description' => 'Show search in header'
422
423
  }]
@@ -452,7 +453,548 @@ describe Dugway::Theme do
452
453
  theme.valid?.should be(false)
453
454
  theme.errors.first.should match(/Missing required color settings:/)
454
455
  end
456
+
457
+ it "validates feature flag format and values" do
458
+ settings = theme.settings.merge({
459
+ 'options' => [{
460
+ 'variable' => 'show_foo',
461
+ 'section' => 'general',
462
+ 'description' => 'Show foo thing',
463
+ 'requires' => [
464
+ 'feature:', # invalid: empty
465
+ 'feature:foo_flag gt visible', # invalid operator
466
+ 'feature:foo_flag eq invalid_value', # invalid value
467
+ 'feature:foo_flag neq visible', # valid
468
+ 'feature:foo_flag eq visible' # valid
469
+ ]
470
+ }]
471
+ })
472
+ allow(theme).to receive(:settings).and_return(settings)
473
+
474
+ theme.valid?.should be(false)
475
+ theme.errors.should include("Option 'show_foo' has invalid feature flag format")
476
+ theme.errors.should include("Option 'show_foo' has invalid operator 'gt'. Feature flags can only use `eq` or `neq`.")
477
+ theme.errors.should include("Option 'show_foo' has invalid comparison value 'invalid_value'. Feature flags can only check for `visible`.")
478
+
479
+ end
480
+
481
+ it "allows options that require image settings" do
482
+ settings = theme.settings.merge({
483
+ 'options' => [{
484
+ 'variable' => 'show_header_text',
485
+ 'section' => 'general',
486
+ 'description' => 'Show header text when logo is not present',
487
+ 'requires' => 'logo'
488
+ }],
489
+ 'images' => [{
490
+ 'variable' => 'logo',
491
+ 'section' => 'general',
492
+ 'description' => 'Site logo'
493
+ }]
494
+ })
495
+ allow(theme).to receive(:settings).and_return(settings)
496
+
497
+ theme.valid?.should be(true)
498
+ theme.errors.should be_empty
499
+ end
500
+
501
+ it "allows options that require image_sets settings" do
502
+ settings = theme.settings.merge({
503
+ 'options' => [{
504
+ 'variable' => 'show_gallery',
505
+ 'section' => 'homepage',
506
+ 'description' => 'Show image gallery',
507
+ 'requires' => 'gallery_images'
508
+ }],
509
+ 'image_sets' => [{
510
+ 'variable' => 'gallery_images',
511
+ 'section' => 'homepage',
512
+ 'description' => 'Gallery images'
513
+ }]
514
+ })
515
+ allow(theme).to receive(:settings).and_return(settings)
516
+
517
+ theme.valid?.should be(true)
518
+ theme.errors.should be_empty
519
+ end
520
+
521
+ it "fails when requires references unknown image setting" do
522
+ settings = theme.settings.merge({
523
+ 'options' => [{
524
+ 'variable' => 'show_banner',
525
+ 'section' => 'general',
526
+ 'description' => 'Show banner image',
527
+ 'requires' => 'unknown_image'
528
+ }]
529
+ })
530
+ allow(theme).to receive(:settings).and_return(settings)
531
+
532
+ theme.valid?.should be(false)
533
+ theme.errors.should include("Option 'show_banner' requires unknown setting 'unknown_image'")
534
+ end
535
+
536
+ describe "when validating object-based requires" do
537
+ let(:base_settings) do
538
+ {
539
+ 'name' => 'Test Theme',
540
+ 'version' => '1.2.3',
541
+ 'colors' => required_colors.map { |color| {'variable' => color} },
542
+ 'options' => [],
543
+ 'images' => [{'variable' => 'slideshow_image', 'section' => 'homepage'}],
544
+ 'image_sets' => [{'variable' => 'hero_images', 'section' => 'homepage'}]
545
+ }
546
+ end
547
+
548
+ before(:each) do
549
+ allow(theme).to receive(:validate_required_color_settings) { true }
550
+ end
551
+
552
+ it "validates 'any' condition with valid settings" do
553
+ settings = base_settings.merge({
554
+ 'options' => [{
555
+ 'variable' => 'show_hero',
556
+ 'section' => 'homepage',
557
+ 'description' => 'Show hero section',
558
+ 'requires' => [
559
+ {
560
+ 'any' => [
561
+ 'slideshow_image present',
562
+ 'hero_images present'
563
+ ]
564
+ }
565
+ ]
566
+ }]
567
+ })
568
+ allow(theme).to receive(:settings).and_return(settings)
569
+
570
+ theme.valid?.should be(true)
571
+ theme.errors.should be_empty
572
+ end
573
+
574
+ it "validates mixed array with both objects and strings" do
575
+ settings = base_settings.merge({
576
+ 'options' => [{
577
+ 'variable' => 'show_advanced_hero',
578
+ 'section' => 'homepage',
579
+ 'description' => 'Show advanced hero',
580
+ 'requires' => [
581
+ {
582
+ 'any' => ['slideshow_image present', 'hero_images present']
583
+ },
584
+ 'inventory'
585
+ ]
586
+ }]
587
+ })
588
+ allow(theme).to receive(:settings).and_return(settings)
589
+
590
+ theme.valid?.should be(true)
591
+ theme.errors.should be_empty
592
+ end
593
+
594
+ it "fails when 'any' condition references unknown settings" do
595
+ settings = base_settings.merge({
596
+ 'options' => [{
597
+ 'variable' => 'show_gallery',
598
+ 'section' => 'homepage',
599
+ 'description' => 'Show image gallery',
600
+ 'requires' => [
601
+ {
602
+ 'any' => [
603
+ 'unknown_image present',
604
+ 'slideshow_image present'
605
+ ]
606
+ }
607
+ ]
608
+ }]
609
+ })
610
+ allow(theme).to receive(:settings).and_return(settings)
611
+
612
+ theme.valid?.should be(false)
613
+ theme.errors.should include("Option 'show_gallery' requires unknown setting 'unknown_image'")
614
+ end
615
+
616
+ it "fails when 'any' condition is not an array" do
617
+ settings = base_settings.merge({
618
+ 'options' => [{
619
+ 'variable' => 'show_content',
620
+ 'section' => 'homepage',
621
+ 'description' => 'Show content',
622
+ 'requires' => [
623
+ {
624
+ 'any' => 'slideshow_image present'
625
+ }
626
+ ]
627
+ }]
628
+ })
629
+ allow(theme).to receive(:settings).and_return(settings)
630
+
631
+ theme.valid?.should be(false)
632
+ theme.errors.should include("Option 'show_content' has invalid 'any' condition - must be an array of rules")
633
+ end
634
+
635
+ it "fails when 'any' condition is empty" do
636
+ settings = base_settings.merge({
637
+ 'options' => [{
638
+ 'variable' => 'show_empty_any',
639
+ 'section' => 'homepage',
640
+ 'description' => 'Show with empty any',
641
+ 'requires' => [
642
+ {
643
+ 'any' => []
644
+ }
645
+ ]
646
+ }]
647
+ })
648
+ allow(theme).to receive(:settings).and_return(settings)
649
+
650
+ theme.valid?.should be(false)
651
+ theme.errors.should include("Option 'show_empty_any' has empty 'any' condition - must contain at least one rule")
652
+ end
653
+
654
+ it "fails when object has unsupported operator" do
655
+ settings = base_settings.merge({
656
+ 'options' => [{
657
+ 'variable' => 'show_unsupported',
658
+ 'section' => 'homepage',
659
+ 'description' => 'Show with unsupported operator',
660
+ 'requires' => [
661
+ {
662
+ 'all' => ['slideshow_image present']
663
+ }
664
+ ]
665
+ }]
666
+ })
667
+ allow(theme).to receive(:settings).and_return(settings)
668
+
669
+ theme.valid?.should be(false)
670
+ theme.errors.should include("Option 'show_unsupported' has unsupported requires object. Only 'any' is currently supported.")
671
+ end
672
+
673
+ it "validates nested operators and values within 'any'" do
674
+ settings = base_settings.merge({
675
+ 'options' => [
676
+ {
677
+ 'variable' => 'enable_slideshow',
678
+ 'section' => 'homepage',
679
+ 'description' => 'Enable slideshow',
680
+ 'type' => 'boolean',
681
+ 'default' => true
682
+ },
683
+ {
684
+ 'variable' => 'show_conditional_hero',
685
+ 'section' => 'homepage',
686
+ 'description' => 'Show conditional hero',
687
+ 'requires' => [
688
+ {
689
+ 'any' => [
690
+ 'enable_slideshow eq true',
691
+ 'slideshow_image present'
692
+ ]
693
+ }
694
+ ]
695
+ }
696
+ ]
697
+ })
698
+ allow(theme).to receive(:settings).and_return(settings)
699
+
700
+ theme.valid?.should be(true)
701
+ theme.errors.should be_empty
702
+ end
703
+
704
+ it "validates feature flags within 'any' condition" do
705
+ settings = base_settings.merge({
706
+ 'options' => [{
707
+ 'variable' => 'show_feature_dependent',
708
+ 'section' => 'homepage',
709
+ 'description' => 'Show feature dependent content',
710
+ 'requires' => [
711
+ {
712
+ 'any' => [
713
+ 'feature:advanced_hero eq visible',
714
+ 'slideshow_image present'
715
+ ]
716
+ }
717
+ ]
718
+ }]
719
+ })
720
+ allow(theme).to receive(:settings).and_return(settings)
721
+
722
+ theme.valid?.should be(true)
723
+ theme.errors.should be_empty
724
+ end
725
+ end
726
+ end
727
+
728
+ describe "when validating option sections" do
729
+ let(:valid_sections_string) { Dugway::Theme::VALID_SECTIONS.join(', ') }
730
+ let(:base_settings) do
731
+ {
732
+ 'name' => 'Test Theme',
733
+ 'version' => '1.2.3',
734
+ 'colors' => required_colors.map { |color| {'variable' => color} },
735
+ 'options' => []
736
+ }
737
+ end
738
+
739
+ # Define required_colors here if not already available in this scope
740
+ let(:required_colors) {[
741
+ 'background_color',
742
+ 'primary_text_color',
743
+ 'link_text_color',
744
+ 'link_hover_color',
745
+ 'button_background_color',
746
+ 'button_text_color',
747
+ 'button_hover_background_color'
748
+ ]}
749
+
750
+ # Fix for the failing tests - define constant if needed
751
+ before(:each) do
752
+ unless defined?(Dugway::Theme::REQUIRED_FILES)
753
+ stub_const("Dugway::Theme::REQUIRED_FILES", [
754
+ "cart.html", "contact.html", "home.html", "layout.html", "maintenance.html",
755
+ "product.html", "products.html", "screenshot.jpg", "settings.json", "theme.css", "theme.js"
756
+ ])
757
+ end
758
+ end
759
+
760
+ it "passes with a valid section" do
761
+ settings = base_settings.merge({
762
+ 'options' => [{
763
+ 'variable' => 'valid_option',
764
+ 'description' => 'Valid option',
765
+ 'section' => 'homepage'
766
+ }]
767
+ })
768
+ # Stub settings
769
+ allow(theme).to receive(:settings).and_return(settings)
770
+
771
+ # Stub ALL required files first
772
+ Dugway::Theme::REQUIRED_FILES.each do |file|
773
+ allow(theme).to receive(:read_source_file).with(file).and_return("some content")
774
+ end
775
+
776
+ # Then override the specific stub for layout.html
777
+ allow(theme).to receive(:read_source_file).with('layout.html')
778
+ .and_return('<body data-bc-page-type="home"><div data-bc-hook="header"></div><div data-bc-hook="footer"></div></body>')
779
+
780
+ # Stub color validation
781
+ allow(theme).to receive(:validate_required_color_settings).and_return(true)
782
+
783
+ expect(Kernel).not_to receive(:warn)
784
+ theme.valid?.should be(true)
785
+ end
786
+
787
+ it "warns appropriately for multiple options with missing or invalid sections" do
788
+ settings = base_settings.merge({
789
+ 'options' => [
790
+ { 'variable' => 'option_1_valid', 'description' => 'Valid', 'section' => 'homepage' },
791
+ { 'variable' => 'option_2_missing', 'description' => 'Missing section' }, # Missing section
792
+ { 'variable' => 'option_3_invalid', 'description' => 'Invalid section', 'section' => 'bad_section' }, # Invalid section
793
+ { 'variable' => 'option_4_valid', 'description' => 'Valid again', 'section' => 'general' },
794
+ { 'variable' => 'option_5_missing', 'description' => 'Missing section again' }, # Missing section again
795
+ { 'description' => 'Option 6 missing variable and section' }, # Missing variable and section
796
+ { 'variable' => 'option_7_invalid', 'description' => 'Invalid section again', 'section' => 'another_bad_one' } # Invalid section again
797
+ ]
798
+ })
799
+ # Stub settings
800
+ allow(theme).to receive(:settings).and_return(settings)
801
+
802
+ # Stub ALL required files first
803
+ Dugway::Theme::REQUIRED_FILES.each do |file|
804
+ allow(theme).to receive(:read_source_file).with(file).and_return("some content")
805
+ end
806
+
807
+ # Then override the specific stub for layout.html
808
+ allow(theme).to receive(:read_source_file).with('layout.html')
809
+ .and_return('<body data-bc-page-type="home"><div data-bc-hook="header"></div><div data-bc-hook="footer"></div></body>')
810
+
811
+ # Stub color validation
812
+ allow(theme).to receive(:validate_required_color_settings).and_return(true)
813
+
814
+ # Allow warn to be called so we can spy on it
815
+ allow(Kernel).to receive(:warn)
816
+
817
+ # Call valid? and assert true (since warnings don't invalidate)
818
+ theme.valid?.should be(true)
819
+
820
+ # Verify calls
821
+ expect(Kernel).to have_received(:warn).with("Warning: Theme setting 'option_2_missing' is missing the 'section' property.")
822
+ expect(Kernel).to have_received(:warn).with("Warning: Theme setting 'option_3_invalid' has an invalid 'section' value: 'bad_section'. Allowed values are: #{valid_sections_string}.")
823
+ expect(Kernel).to have_received(:warn).with("Warning: Theme setting 'option_5_missing' is missing the 'section' property.")
824
+ expect(Kernel).to have_received(:warn).with("Warning: Theme setting 'unknown' is missing the 'section' property.")
825
+ expect(Kernel).to have_received(:warn).with("Warning: Theme setting 'option_7_invalid' has an invalid 'section' value: 'another_bad_one'. Allowed values are: #{valid_sections_string}.")
826
+ end
827
+
828
+ it "handles options without a variable name gracefully when section is invalid" do
829
+ settings = base_settings.merge({
830
+ 'options' => [{
831
+ # 'variable' key is missing
832
+ 'description' => 'Option without variable',
833
+ 'section' => 'invalid_area' # Invalid section to trigger warning
834
+ }]
835
+ })
836
+
837
+ # Stub all necessary methods
838
+ allow(theme).to receive(:settings).and_return(settings)
839
+ allow(theme).to receive(:validate_required_color_settings).and_return(true)
840
+ allow(theme).to receive(:validate_required_layout_attributes).and_return(true)
841
+ allow(theme).to receive(:validate_options_requires).and_return(true)
842
+ allow(theme).to receive(:validate_options_descriptions).and_return(true)
843
+ allow(theme).to receive(:validate_option_defaults).and_return(true)
844
+
845
+ # Stub file checks - this is crucial
846
+ Dugway::Theme::REQUIRED_FILES.each do |file|
847
+ allow(theme).to receive(:read_source_file).with(file).and_return("some content")
848
+ end
849
+
850
+ # Allow warn to be called
851
+ allow(Kernel).to receive(:warn)
852
+
853
+ # Run the validation
854
+ theme.valid?.should be(true)
855
+
856
+ # Check that the warning was issued
857
+ expect(Kernel).to have_received(:warn).with("Warning: Theme setting 'unknown' has an invalid 'section' value: 'invalid_area'. Allowed values are: #{valid_sections_string}.")
858
+ end
859
+
860
+ it "handles options without a variable name gracefully when section is missing" do
861
+ settings = base_settings.merge({
862
+ 'options' => [{
863
+ # 'variable' key is missing
864
+ 'description' => 'Option without variable or section'
865
+ # 'section' key is missing
866
+ }]
867
+ })
868
+
869
+ # Stub all necessary methods
870
+ allow(theme).to receive(:settings).and_return(settings)
871
+ allow(theme).to receive(:validate_required_color_settings).and_return(true)
872
+ allow(theme).to receive(:validate_required_layout_attributes).and_return(true)
873
+ allow(theme).to receive(:validate_options_requires).and_return(true)
874
+ allow(theme).to receive(:validate_options_descriptions).and_return(true)
875
+ allow(theme).to receive(:validate_option_defaults).and_return(true)
876
+
877
+ # Stub file checks
878
+ Dugway::Theme::REQUIRED_FILES.each do |file|
879
+ allow(theme).to receive(:read_source_file).with(file).and_return("some content")
880
+ end
881
+
882
+ # Allow warn to be called
883
+ allow(Kernel).to receive(:warn)
884
+
885
+ # Run the validation
886
+ theme.valid?.should be(true)
887
+
888
+ # Check that the warning was issued
889
+ expect(Kernel).to have_received(:warn).with("Warning: Theme setting 'unknown' is missing the 'section' property.")
890
+ end
891
+ end
892
+
893
+ end
894
+
895
+ shared_examples "option value validation" do |value_type|
896
+ context "when type is 'select' with explicit options" do
897
+ let(:options) do
898
+ [{
899
+ 'type' => 'select',
900
+ 'variable' => 'hero_overlay_opacity',
901
+ 'options' => [['Bright', 'bright'], ['Dark', 'dark']],
902
+ value_type => 'bright'
903
+ }]
904
+ end
905
+
906
+ it "passes when #{value_type} matches a valid option" do
907
+ theme.instance_variable_set(:@errors, [])
908
+ theme.send(:validate_option_defaults)
909
+ expect(theme.errors).to be_empty
910
+ end
911
+
912
+ it "flags invalid #{value_type} values" do
913
+ options.first[value_type] = 'invalid'
914
+ theme.instance_variable_set(:@errors, [])
915
+ theme.send(:validate_option_defaults)
916
+ expect(theme.errors).to include("#{value_type.capitalize} 'invalid' is not a valid option for hero_overlay_opacity")
917
+ end
455
918
  end
919
+
920
+ context "when type is 'select' with numeric range" do
921
+ let(:options) do
922
+ [{
923
+ 'type' => 'select',
924
+ 'variable' => 'featured_products',
925
+ 'options' => '0..100',
926
+ value_type => 50
927
+ }]
928
+ end
929
+
930
+ it "passes when #{value_type} is within range" do
931
+ theme.instance_variable_set(:@errors, [])
932
+ theme.send(:validate_option_defaults)
933
+ expect(theme.errors).to be_empty
934
+ end
935
+
936
+ it "flags #{value_type} outside the range" do
937
+ options.first[value_type] = 101
938
+ theme.instance_variable_set(:@errors, [])
939
+ theme.send(:validate_option_defaults)
940
+ expect(theme.errors).to include("#{value_type.capitalize} '101' is out of range for featured_products")
941
+ end
942
+ end
943
+
944
+ context "when type is 'boolean'" do
945
+ let(:options) do
946
+ [{
947
+ 'type' => 'boolean',
948
+ 'variable' => 'show_home_page_categories',
949
+ value_type => true
950
+ }]
951
+ end
952
+
953
+ it "passes when #{value_type} is a valid boolean" do
954
+ theme.instance_variable_set(:@errors, [])
955
+ theme.send(:validate_option_defaults)
956
+ expect(theme.errors).to be_empty
957
+ end
958
+
959
+ it "flags non-boolean #{value_type}" do
960
+ options.first[value_type] = 'true'
961
+ theme.instance_variable_set(:@errors, [])
962
+ theme.send(:validate_option_defaults)
963
+ expect(theme.errors).to include("#{value_type.capitalize} 'true' is not a boolean for show_home_page_categories")
964
+ end
965
+ end
966
+
967
+ context "when type is 'select' with product_orders" do
968
+ let(:options) do
969
+ [{
970
+ 'type' => 'select',
971
+ 'variable' => 'related_products_order',
972
+ 'options' => 'product_orders',
973
+ value_type => 'top-selling'
974
+ }]
975
+ end
976
+
977
+ it "passes when #{value_type} matches a valid product_orders value" do
978
+ theme.instance_variable_set(:@errors, [])
979
+ theme.send(:validate_option_defaults)
980
+ expect(theme.errors).to be_empty
981
+ end
982
+
983
+ it "flags invalid #{value_type} values" do
984
+ options.first[value_type] = 'invalid'
985
+ theme.instance_variable_set(:@errors, [])
986
+ theme.send(:validate_option_defaults)
987
+ expect(theme.errors).to include("#{value_type.capitalize} 'invalid' is not a valid option for related_products_order")
988
+ end
989
+ end
990
+ end
991
+
992
+ describe "#validate_option_defaults" do
993
+ let(:settings) { { 'options' => options } }
994
+ before { allow(theme).to receive(:settings).and_return(settings) }
995
+
996
+ it_behaves_like "option value validation", 'default'
997
+ it_behaves_like "option value validation", 'upgrade_default'
456
998
  end
457
999
 
458
1000
  def read_file(file_name)