spree_variant_options 0.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,56 @@
1
+ # IMPORTANT: This file is generated by cucumber-rails - edit at your own peril.
2
+ # It is recommended to regenerate this file in the future when you upgrade to a
3
+ # newer version of cucumber-rails. Consider adding your own code to a new file
4
+ # instead of editing this one. Cucumber will automatically load all features/**/*.rb
5
+ # files.
6
+
7
+ require 'spork'
8
+
9
+ ENV["RAILS_ROOT"] = File.expand_path("../../../test/dummy", __FILE__)
10
+
11
+ Spork.prefork do
12
+ require 'cucumber/rails'
13
+ require 'capybara/rails'
14
+ require 'capybara/cucumber'
15
+ require 'capybara/session'
16
+ require 'factory_girl'
17
+ require 'faker'
18
+
19
+ I18n.reload!
20
+
21
+ Capybara.default_driver = :selenium
22
+ Capybara.default_selector = :css
23
+ end
24
+
25
+ Spork.each_run do
26
+
27
+ # By default, any exception happening in your Rails application will bubble up
28
+ # to Cucumber so that your scenario will fail. This is a different from how
29
+ # your application behaves in the production environment, where an error page will
30
+ # be rendered instead.
31
+ #
32
+ # Sometimes we want to override this default behaviour and allow Rails to rescue
33
+ # exceptions and display an error page (just like when the app is running in production).
34
+ # Typical scenarios where you want to do this is when you test your error pages.
35
+ # There are two ways to allow Rails to rescue exceptions:
36
+ #
37
+ # 1) Tag your scenario (or feature) with @allow-rescue
38
+ #
39
+ # 2) Set the value below to true. Beware that doing this globally is not
40
+ # recommended as it will mask a lot of errors for you!
41
+ #
42
+ ActionController::Base.allow_rescue = false
43
+
44
+ # doesn't seem to work :/
45
+ Cucumber::Rails::World.use_transactional_fixtures = false
46
+ # Remove/comment out the lines below if your app doesn't have a database.
47
+ # For some databases (like MongoDB and CouchDB) you may need to use :truncation instead.
48
+ begin
49
+ DatabaseCleaner.strategy = :transaction
50
+ rescue NameError
51
+ raise "You need to add database_cleaner to your Gemfile (in the :test group) if you wish to use it."
52
+ end
53
+
54
+ Dir["#{File.expand_path("../../../", __FILE__)}/test/support/**/*.rb"].each { |f| require f }
55
+
56
+ end
@@ -0,0 +1,45 @@
1
+ FactoryGirl.define do
2
+
3
+ factory :product do
4
+ name "Very Wearily Variantly"
5
+ description "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh."
6
+ available_on { Time.zone.now - 1.day }
7
+ permalink "very-wearily-variantly"
8
+ price 17.00
9
+ count_on_hand 10
10
+ end
11
+
12
+ factory :product_with_variants, :parent => :product do
13
+ after_create { |product|
14
+ sizes = %w(Small Medium Large X-Large).map{|i| Factory.create(:option_value, :presentation => i) }
15
+ colors = %w(Red Green Blue Yellow Purple Gray Black White).map{|i|
16
+ Factory.create(:option_value, :presentation => i, :option_type => OptionType.find_by_name("color") || Factory.create(:option_type, :presentation => "Color"))
17
+ }
18
+ product.variants = sizes.map{|i| colors.map{|j| Factory.create(:variant, :product => product, :option_values => [i, j]) }}.flatten
19
+ product.option_types = OptionType.where(:name => %w(size color))
20
+ }
21
+ end
22
+
23
+ factory :variant do
24
+ product { Product.last || Factory.create(:product) }
25
+ option_values { [OptionValue.last || Factory.create(:option_value)] }
26
+ sequence(:sku) { |n| "ROR-#{1000 + n}" }
27
+ sequence(:price) { |n| 19.99 + n }
28
+ cost_price 17.00
29
+ count_on_hand 10
30
+ end
31
+
32
+ factory :option_type do
33
+ presentation "Size"
34
+ name { presentation.downcase }
35
+ sequence(:position) {|n| n }
36
+ end
37
+
38
+ factory :option_value do
39
+ presentation "Large"
40
+ name { presentation.downcase }
41
+ option_type { OptionType.last || Factory.create(:option_type) }
42
+ sequence(:position) {|n| n }
43
+ end
44
+
45
+ end
@@ -0,0 +1,28 @@
1
+ module HelperMethods
2
+
3
+ # Checks for missing translations after each test
4
+ def teardown
5
+ unless source.blank?
6
+ matches = source.match(/translation[\s-]+missing[^"]*/) || []
7
+ assert_equal 0, matches.length, "Translation Missing! - #{matches[0]}"
8
+ end
9
+ end
10
+
11
+ # An assertion for ensuring content has made it to the page.
12
+ #
13
+ # assert_seen "Site Title"
14
+ # assert_seen "Peanut Butter Jelly Time", :within => ".post-title h1"
15
+ #
16
+ def assert_seen(text, opts={})
17
+ if opts[:within]
18
+ within(opts[:within]) do
19
+ assert has_content?(text)
20
+ end
21
+ else
22
+ assert has_content?(text)
23
+ end
24
+ end
25
+
26
+ end
27
+
28
+ World(HelperMethods)
@@ -0,0 +1,36 @@
1
+ module NavigationHelpers
2
+ # Maps a name to a path. Used by the
3
+ #
4
+ # When /^I go to (.+)$/ do |page_name|
5
+ #
6
+ # step definition in web_steps.rb
7
+ #
8
+ def path_to(page_name)
9
+ case page_name
10
+
11
+ when /the home\s?page/
12
+ '/'
13
+ when /the new post page/
14
+ new_post_path
15
+
16
+
17
+ # Add more mappings here.
18
+ # Here is an example that pulls values out of the Regexp:
19
+ #
20
+ # when /^(.*)'s profile page$/i
21
+ # user_profile_path(User.find_by_login($1))
22
+
23
+ else
24
+ begin
25
+ page_name =~ /the (.*) page/
26
+ path_components = $1.split(/\s+/)
27
+ self.send(path_components.push('path').join('_').to_sym)
28
+ rescue Object => e
29
+ raise "Can't find mapping from \"#{page_name}\" to a path.\n" +
30
+ "Now, go and add a mapping in #{__FILE__}"
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ World(NavigationHelpers)
@@ -0,0 +1,39 @@
1
+ module HtmlSelectorsHelpers
2
+ # Maps a name to a selector. Used primarily by the
3
+ #
4
+ # When /^(.+) within (.+)$/ do |step, scope|
5
+ #
6
+ # step definitions in web_steps.rb
7
+ #
8
+ def selector_for(locator)
9
+ case locator
10
+
11
+ when /the page/
12
+ "html > body"
13
+
14
+ # Add more mappings here.
15
+ # Here is an example that pulls values out of the Regexp:
16
+ #
17
+ # when /the (notice|error|info) flash/
18
+ # ".flash.#{$1}"
19
+
20
+ # You can also return an array to use a different selector
21
+ # type, like:
22
+ #
23
+ # when /the header/
24
+ # [:xpath, "//header"]
25
+
26
+ # This allows you to provide a quoted selector as the scope
27
+ # for "within" steps as was previously the default for the
28
+ # web steps:
29
+ when /"(.+)"/
30
+ $1
31
+
32
+ else
33
+ raise "Can't find mapping from \"#{locator}\" to a selector.\n" +
34
+ "Now, go and add a mapping in #{__FILE__}"
35
+ end
36
+ end
37
+ end
38
+
39
+ World(HtmlSelectorsHelpers)
@@ -0,0 +1,108 @@
1
+ @no-txn @javascript
2
+ Feature: Products should have variant options
3
+ A Product's variants should be broken out into options
4
+
5
+ Scenario: Display options when visiting a product
6
+ Given I have a product with variants
7
+ And I'm on the product page for the first product
8
+ Then the source should contain the options hash
9
+ And I should see enabled links for the first option type
10
+ And I should see disabled links for the second option type
11
+ And I should have a hidden input for the selected variant
12
+ And the add to cart button should be disabled
13
+
14
+ Scenario: Interact with options for a product
15
+ Given I have a product with variants
16
+ And I'm on the product page for the first product
17
+ When I follow "Small" within the first set of options
18
+ Then I should see enabled links for the second option type
19
+ And the add to cart button should be disabled
20
+ When I follow "Green" within the second set of options
21
+ Then the add to cart button should be enabled
22
+ When I follow "Medium" within the first set of options
23
+ And I should see enabled links for the second option type
24
+ And the add to cart button should be disabled
25
+ When I follow "Red" within the second set of options
26
+ And the add to cart button should be enabled
27
+
28
+ Scenario: Should show out of stock for appropriate variants
29
+ Given I have a product with variants
30
+ And the "Small Green" variant is out of stock
31
+ And I'm on the product page for the first product
32
+ When I follow "Small" within the first set of options
33
+ Then I should see an out-of-stock link for "Green"
34
+ And I should see an in-stock link for "Red, Blue, Black, White, Gray"
35
+
36
+ Scenario: Should clear current selection
37
+ Given I have a product with variants
38
+ And I'm on the product page for the first product
39
+ When I follow "Small" within the first set of options
40
+ And I click the current clear button
41
+ Then I should see disabled links for the second option type
42
+ And I should see enabled links for the first option type
43
+
44
+ Scenario: Should clear current selection and maintain parent selection
45
+ Given I have a product with variants
46
+ And I'm on the product page for the first product
47
+ When I follow "Small" within the first set of options
48
+ And I follow "Green" within the second set of options
49
+ Then the add to cart button should be enabled
50
+ And I click the last clear button
51
+ Then I should see "Small" selected within the first set of options
52
+ And I should see enabled links for the second option type
53
+ And the add to cart button should be disabled
54
+
55
+ Scenario: Should clear current selection and parent selection
56
+ Given I have a product with variants
57
+ And I'm on the product page for the first product
58
+ When I follow "Small" within the first set of options
59
+ And I follow "Green" within the second set of options
60
+ Then the add to cart button should be enabled
61
+ And I click the first clear button
62
+ Then I should not see a selected option
63
+ And I should see disabled links for the second option type
64
+ And I should see enabled links for the first option type
65
+ And the add to cart button should be disabled
66
+
67
+ Scenario: Should add proper variant to cart
68
+ Given I have a product with variants
69
+ And I'm on the product page for the first product
70
+ When I follow "Small" within the first set of options
71
+ And I follow "Green" within the second set of options
72
+ Then the add to cart button should be enabled
73
+ And I press "Add To Cart"
74
+ Then I should be on the cart page
75
+ And I should see "Size: Small, Color: Green"
76
+
77
+ Scenario: Should auto-select variant if its the only option
78
+ Given I have a product with variants
79
+ And I have an "XXL Turquoise" variant
80
+ And I'm on the product page for the first product
81
+ When I follow "XXL" within the first set of options
82
+ Then I should see "Turquoise" selected within the second set of options
83
+ And the add to cart button should be enabled
84
+ When I follow "Small" within the first set of options
85
+ Then the add to cart button should be disabled
86
+
87
+ Scenario: Should adjust price according to variant
88
+ Given I have a product with variants
89
+ And I have an "XXS Turquoise" variant for $29.99
90
+ And I have an "XXS Pink" variant for $24.99
91
+ And I'm on the product page for the first product
92
+ When I follow "XXS" within the first set of options
93
+ Then I should see "$24.99 - $29.99" in the price
94
+ When I follow "Turquoise" within the second set of options
95
+ Then I should see "$29.99" in the price
96
+ And the add to cart button should be enabled
97
+ When I follow "Pink" within the second set of options
98
+ Then I should see "$24.99" in the price
99
+ And the add to cart button should be enabled
100
+
101
+ Scenario: Should show variant images when a selection is made
102
+ Given I have a product with variants
103
+ And I'm on the product page for the first product
104
+ When I follow "Small" within the first set of options
105
+ And I follow "Green" within the second set of options
106
+ Then the add to cart button should be enabled
107
+ And I should see "Small Green" in the variant images label # its hidden but it's there!
108
+
@@ -0,0 +1 @@
1
+ rake "db:migrate db:seed db:sample", :env => "development"
@@ -0,0 +1 @@
1
+ rake "spree_core:install spree_sample:install"
@@ -0,0 +1,26 @@
1
+ require 'spree_core'
2
+ require 'spree_sample' unless Rails.env == 'production'
3
+
4
+ module SpreeVariantOptions
5
+
6
+ class Engine < Rails::Engine
7
+
8
+ config.autoload_paths += %W(#{config.root}/lib)
9
+
10
+ initializer "static assets" do |app|
11
+ app.middleware.insert_before ::Rack::Lock, ::ActionDispatch::Static, "#{config.root}/public"
12
+ end
13
+
14
+ def self.activate
15
+
16
+ Dir.glob(File.join(File.dirname(__FILE__), "../app/**/*_decorator.rb")) do |c|
17
+ Rails.env.production? ? require(c) : load(c)
18
+ end
19
+
20
+ end
21
+
22
+ config.to_prepare &method(:activate).to_proc
23
+
24
+ end
25
+
26
+ end
@@ -0,0 +1,3 @@
1
+ module SpreeVariantOptions
2
+ VERSION = "0.1.0.rc1"
3
+ end
@@ -0,0 +1,38 @@
1
+ var add_image_handlers = function() {
2
+ $("#main-image").data('selectedThumb', $('#main-image img').attr('src'));
3
+ $('ul.thumbnails li').eq(0).addClass('selected');
4
+ $('ul.thumbnails li a').click(function() {
5
+ $("#main-image").data('selectedThumb', $(this).attr('href'));
6
+ $('ul.thumbnails li').removeClass('selected');
7
+ $(this).parent('li').addClass('selected');
8
+ return false;
9
+ }).hover(function() {
10
+ $('#main-image img').attr('src', $(this).attr('href').replace('mini', 'product'));
11
+ }, function() {
12
+ $('#main-image img').attr('src', $("#main-image").data('selectedThumb'));
13
+ });
14
+ };
15
+
16
+ var select_variant = function(vid, text) {
17
+ jQuery("#variant-thumbnails").empty();
18
+ jQuery("#variant-images span").html(text);
19
+ if (images[vid].length > 0) {
20
+ $.each(images[vid], function(i, link) {
21
+ jQuery("#variant-thumbnails").append('<li>' + link + '</li>');
22
+ });
23
+ jQuery("#variant-images").show();
24
+ } else {
25
+ jQuery("#variant-images").hide();
26
+ }
27
+ add_image_handlers();
28
+ var link = jQuery("#variant-thumbnails a")[0];
29
+ jQuery("#main-image img").attr({src: jQuery(link).attr('href')});
30
+ jQuery('ul.thumbnails li').removeClass('selected');
31
+ jQuery(link).parent('li').addClass('selected');
32
+ }
33
+
34
+ jQuery(document).ready(function() {
35
+ add_image_handlers();
36
+ jQuery("#variant-thumbnails").empty();
37
+ jQuery("#variant-images").hide();
38
+ });
@@ -0,0 +1,211 @@
1
+ $.extend({
2
+ keys: function(obj){
3
+ var a = [];
4
+ $.each(obj, function(k){ a.push(k) });
5
+ return a;
6
+ }
7
+ });
8
+
9
+ if (!Array.indexOf) Array.prototype.indexOf = function(obj) {
10
+ for(var i = 0; i < this.length; i++){
11
+ if(this[i] == obj) {
12
+ return i;
13
+ }
14
+ }
15
+ return -1;
16
+ }
17
+
18
+ if (!Array.find_matches) Array.find_matches = function(a) {
19
+ var i, m = [];
20
+ a = a.sort();
21
+ i = a.length
22
+ while(i--) {
23
+ if (a[i - 1] == a[i]) {
24
+ m.push(a[i]);
25
+ }
26
+ }
27
+ if (m.length == 0) {
28
+ return false;
29
+ }
30
+ return m;
31
+ }
32
+
33
+ function VariantOptions(options) {
34
+
35
+ var options = options;
36
+ var variant, divs, parent, index = 0;
37
+ var selection = [];
38
+
39
+ function init() {
40
+ divs = $('#product-variants .variant-options');
41
+ disable(divs.find('a.option-value').addClass('locked'));
42
+ update();
43
+ enable(parent.find('a.option-value'));
44
+ toggle();
45
+ $('.clear-option a.clear-button').hide().click(handle_clear);
46
+ }
47
+
48
+ function get_index(parent) {
49
+ return parseInt($(parent).attr('class').replace(/[^\d]/g, ''));
50
+ }
51
+
52
+ function update(i) {
53
+ index = isNaN(i) ? index : i;
54
+ parent = $(divs.get(index));
55
+ buttons = parent.find('a.option-value');
56
+ parent.find('a.clear-button').hide();
57
+ }
58
+
59
+ function disable(btns) {
60
+ return btns.removeClass('selected');
61
+ }
62
+
63
+ function enable(btns) {
64
+ return btns.not('.unavailable').removeClass('locked').unbind('click').filter('.in-stock').click(handle_click).filter('.auto-click').removeClass('auto-click').click();
65
+ }
66
+
67
+ function advance() {
68
+ index++
69
+ update();
70
+ inventory(buttons.removeClass('locked'));
71
+ enable(buttons.filter('.in-stock'));
72
+ }
73
+
74
+ function inventory(btns) {
75
+ var keys, variants, count = 0, selected = {};
76
+ var sels = $.map(divs.find('a.selected'), function(i) { return i.rel });
77
+ $.each(sels, function(key, value) {
78
+ key = value.split('-');
79
+ var v = options[key[0]][key[1]];
80
+ keys = $.keys(v);
81
+ var m = Array.find_matches(selection.concat(keys));
82
+ if (selection.length == 0) {
83
+ selection = keys;
84
+ } else if (m) {
85
+ selection = m;
86
+ }
87
+ });
88
+ btns.removeClass('in-stock out-of-stock unavailable').each(function(i, element) {
89
+ variants = get_variant_objects(element.rel);
90
+ keys = $.keys(variants);
91
+ if (keys.length == 0) {
92
+ disable($(element).addClass('unavailable locked').unbind('click'));
93
+ } else if (keys.length == 1) {
94
+ _var = variants[keys[0]];
95
+ $(element).addClass(_var.count ? selection.length == 1 ? 'in-stock auto-click' : 'in-stock' : 'out-of-stock');
96
+ } else {
97
+ $.each(variants, function(key, value) { count += value.count });
98
+ $(element).addClass(count ? 'in-stock' : 'out-of-stock');
99
+ }
100
+ });
101
+ }
102
+
103
+ function get_variant_objects(rels) {
104
+ var i, ids, obj, variants = {};
105
+ if (typeof(rels) == 'string') { rels = [rels]; }
106
+ var otid, ovid, opt, opv;
107
+ i = rels.length;
108
+ try {
109
+ while (i--) {
110
+ ids = rels[i].split('-');
111
+ otid = ids[0];
112
+ ovid = ids[1];
113
+ opt = options[otid];
114
+ if (opt) {
115
+ opv = opt[ovid];
116
+ ids = $.keys(opv);
117
+ if (opv && ids.length) {
118
+ var j = ids.length;
119
+ while (j--) {
120
+ obj = opv[ids[j]];
121
+ if (obj && $.keys(obj).length && 0 <= selection.indexOf(obj.id.toString())) {
122
+ variants[obj.id] = obj;
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+ } catch(error) {
129
+ //console.log(error);
130
+ }
131
+ return variants;
132
+ }
133
+
134
+ function to_f(string) {
135
+ return parseFloat(string.replace(/[^\d\.]/g, ''));
136
+ }
137
+
138
+ function find_variant() {
139
+ var selected = divs.find('a.selected');
140
+ var variants = get_variant_objects(selected.get(0).rel);
141
+ if (selected.length == divs.length) {
142
+ return variant = variants[selection[0]];
143
+ } else {
144
+ var prices = [];
145
+ $.each(variants, function(key, value) { prices.push(value.price) });
146
+ prices = $.unique(prices).sort(function(a, b) {
147
+ return to_f(a) < to_f(b) ? -1 : 1;
148
+ });
149
+ if (prices.length == 1) {
150
+ $('.prices .price').html('<span class="price assumed">' + prices[0] + '</span>');
151
+ } else {
152
+ $('.prices .price').html('<span class="price from">' + prices[0] + '</span> - <span class="price to">' + prices[prices.length - 1] + '</span>');
153
+ }
154
+ return false;
155
+ }
156
+ }
157
+
158
+ function toggle() {
159
+ if (variant) {
160
+ $('#variant_id').val(variant.id);
161
+ $('.prices .price').removeClass('unselected').text(variant.price);
162
+ $('button[type=submit]').attr('disabled', false).fadeTo(100, 1);
163
+ try {
164
+ select_variant(variant.id, $.map($('a.selected'), function(i) { return $(i).text() }).join(" "));
165
+ } catch(error) {
166
+ // depends on modified version of product.js
167
+ }
168
+ } else {
169
+ $('#variant_id').val('');
170
+ $('button[type=submit]').attr('disabled', true).fadeTo(0, 0.5);
171
+ $('.prices .price').addClass('unselected').text('(select)');
172
+ }
173
+ }
174
+
175
+ function clear(i) {
176
+ variant = null;
177
+ update(i);
178
+ enable(buttons.removeClass('selected'));
179
+ toggle();
180
+ parent.nextAll().each(function(index, element) {
181
+ disable($(element).find('a.option-value').show().removeClass('in-stock out-of-stock').addClass('locked').unbind('click'));
182
+ $(element).find('a.clear-button').hide();
183
+ });
184
+ }
185
+
186
+
187
+ function handle_clear(evt) {
188
+ evt.preventDefault();
189
+ clear(get_index(this));
190
+ }
191
+
192
+ function handle_click(evt) {
193
+ evt.preventDefault();
194
+ variant = null;
195
+ selection = [];
196
+ var a = $(this);
197
+ if (!parent.has(a).length) {
198
+ clear(divs.index(a.parents('.variant-options:first')));
199
+ }
200
+ disable(buttons);
201
+ var a = enable(a.addClass('selected'));
202
+ parent.find('a.clear-button').css('display', 'block');
203
+ advance();
204
+ if (find_variant()) {
205
+ toggle();
206
+ }
207
+ }
208
+
209
+ $(document).ready(init);
210
+
211
+ };