split 3.2.0 → 3.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.travis.yml +13 -1
- data/Appraisals +4 -2
- data/README.md +6 -5
- data/gemfiles/5.0.gemfile +0 -1
- data/gemfiles/5.1.gemfile +0 -1
- data/gemfiles/5.2.gemfile +9 -0
- data/lib/split/configuration.rb +1 -0
- data/lib/split/dashboard.rb +2 -0
- data/lib/split/dashboard/pagination_helpers.rb +87 -0
- data/lib/split/dashboard/paginator.rb +16 -0
- data/lib/split/dashboard/public/style.css +9 -0
- data/lib/split/dashboard/views/index.erb +5 -1
- data/lib/split/helper.rb +5 -1
- data/lib/split/persistence/cookie_adapter.rb +35 -1
- data/lib/split/redis_interface.rb +1 -3
- data/lib/split/version.rb +1 -1
- data/spec/dashboard/pagination_helpers_spec.rb +198 -0
- data/spec/dashboard/paginator_spec.rb +37 -0
- data/spec/helper_spec.rb +8 -0
- data/spec/persistence/cookie_adapter_spec.rb +89 -26
- data/spec/split_spec.rb +7 -7
- data/split.gemspec +3 -3
- metadata +16 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ac2527f0dab7b1f6c2de3e21b02fd933eef302046e7cdd4b8f42da20a3cdf1ba
|
4
|
+
data.tar.gz: 6332eabde240b1c408ff434850b7ac520b3081bfd5fe76f7343259a8eae039b6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 331863e4c78e8dabf7eb23551608be301cdd33d4c14186ab47def00485b028c05784f70009fd66d1b3e240963d1cd45b61d2233b651fd0bcbcb79e2871d2a6ad
|
7
|
+
data.tar.gz: 2c8723105824b9dfc315f88de450be5c18aa32d20e2253a25ca9fcbb6b65b082697c0782719198a2f7b401e66824597a50c3f97dece30a3c9ceccb3fcd62fa95
|
data/.travis.yml
CHANGED
@@ -5,12 +5,15 @@ rvm:
|
|
5
5
|
- 2.1
|
6
6
|
- 2.2.0
|
7
7
|
- 2.2.2
|
8
|
-
- 2.4.
|
8
|
+
- 2.4.3
|
9
|
+
- 2.5.1
|
9
10
|
|
10
11
|
gemfile:
|
11
12
|
- gemfiles/4.2.gemfile
|
12
13
|
- gemfiles/5.0.gemfile
|
13
14
|
- gemfiles/5.1.gemfile
|
15
|
+
- gemfiles/5.2.gemfile
|
16
|
+
|
14
17
|
|
15
18
|
matrix:
|
16
19
|
exclude:
|
@@ -30,6 +33,15 @@ matrix:
|
|
30
33
|
gemfile: gemfiles/5.0.gemfile
|
31
34
|
- rvm: 2.2.0
|
32
35
|
gemfile: gemfiles/5.1.gemfile
|
36
|
+
- rvm: 1.9.3
|
37
|
+
gemfile: gemfiles/5.2.gemfile
|
38
|
+
- rvm: 2.0
|
39
|
+
gemfile: gemfiles/5.2.gemfile
|
40
|
+
- rvm: 2.1
|
41
|
+
gemfile: gemfiles/5.2.gemfile
|
42
|
+
- rvm: 2.2.0
|
43
|
+
gemfile: gemfiles/5.2.gemfile
|
44
|
+
|
33
45
|
|
34
46
|
before_install:
|
35
47
|
- gem update --system && gem install bundler
|
data/Appraisals
CHANGED
@@ -4,10 +4,12 @@ end
|
|
4
4
|
|
5
5
|
appraise "5.0" do
|
6
6
|
gem "rails", "~> 5.0"
|
7
|
-
gem "sinatra", git: "https://github.com/sinatra/sinatra"
|
8
7
|
end
|
9
8
|
|
10
9
|
appraise "5.1" do
|
11
10
|
gem "rails", "~> 5.1"
|
12
|
-
|
11
|
+
end
|
12
|
+
|
13
|
+
appraise "5.2" do
|
14
|
+
gem "rails", "~> 5.2"
|
13
15
|
end
|
data/README.md
CHANGED
@@ -5,6 +5,7 @@
|
|
5
5
|
[![Code Climate](https://codeclimate.com/github/splitrb/split/badges/gpa.svg)](https://codeclimate.com/github/splitrb/split)
|
6
6
|
[![Test Coverage](https://codeclimate.com/github/splitrb/split/badges/coverage.svg)](https://codeclimate.com/github/splitrb/split/coverage)
|
7
7
|
[![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme)
|
8
|
+
[![Open Source Helpers](https://www.codetriage.com/splitrb/split/badges/users.svg)](https://www.codetriage.com/splitrb/split)
|
8
9
|
|
9
10
|
> 📈 The Rack Based A/B testing framework http://libraries.io/rubygems/split
|
10
11
|
|
@@ -388,7 +389,7 @@ run Rack::URLMap.new \
|
|
388
389
|
"/split" => Split::Dashboard.new
|
389
390
|
```
|
390
391
|
|
391
|
-
However, if you are using Rails 3: You can mount this inside your app routes by first adding this to the Gemfile:
|
392
|
+
However, if you are using Rails 3 or higher: You can mount this inside your app routes by first adding this to the Gemfile:
|
392
393
|
|
393
394
|
```ruby
|
394
395
|
gem 'split', require: 'split/dashboard'
|
@@ -475,7 +476,7 @@ Split.configure do |config|
|
|
475
476
|
# IP config
|
476
477
|
config.ignore_ip_addresses << '81.19.48.130' # or regex: /81\.19\.48\.[0-9]+/
|
477
478
|
|
478
|
-
# or provide your own filter functionality, the default is proc{ |request| is_robot? || is_ignored_ip_address? }
|
479
|
+
# or provide your own filter functionality, the default is proc{ |request| is_robot? || is_ignored_ip_address? || is_preview? }
|
479
480
|
config.ignore_filter = -> (request) { CustomExcludeLogic.excludes?(request) }
|
480
481
|
end
|
481
482
|
```
|
@@ -662,8 +663,8 @@ Once you finish one of the goals, the test is considered to be completed, and fi
|
|
662
663
|
**Bad Example**: Test both how button color affects signup *and* how it affects login, at the same time. THIS WILL NOT WORK.
|
663
664
|
|
664
665
|
#### Combined Experiments
|
665
|
-
If you want to test how
|
666
|
-
Configure like so
|
666
|
+
If you want to test how button color affects signup *and* how it affects login at the same time, use combined experiments.
|
667
|
+
Configure like so:
|
667
668
|
```ruby
|
668
669
|
Split.configuration.experiments = {
|
669
670
|
:button_color_experiment => {
|
@@ -934,4 +935,4 @@ Please note that this project is released with a [Contributor Code of Conduct](C
|
|
934
935
|
|
935
936
|
## Copyright
|
936
937
|
|
937
|
-
[MIT License](LICENSE) ©
|
938
|
+
[MIT License](LICENSE) © 2018 [Andrew Nesbitt](https://github.com/andrew).
|
data/gemfiles/5.0.gemfile
CHANGED
data/gemfiles/5.1.gemfile
CHANGED
data/lib/split/configuration.rb
CHANGED
@@ -74,6 +74,7 @@ module Split
|
|
74
74
|
'bitlybot' => 'bit.ly bot',
|
75
75
|
'bot@linkfluence.net' => 'Linkfluence bot',
|
76
76
|
'facebookexternalhit' => 'facebook bot',
|
77
|
+
'Facebot' => 'Facebook crawler',
|
77
78
|
'Feedfetcher-Google' => 'Google Feedfetcher',
|
78
79
|
'https://developers.google.com/+/web/snippet' => 'Google+ Snippet Fetcher',
|
79
80
|
'LinkedInBot' => 'LinkedIn bot',
|
data/lib/split/dashboard.rb
CHANGED
@@ -3,6 +3,7 @@ require 'sinatra/base'
|
|
3
3
|
require 'split'
|
4
4
|
require 'bigdecimal'
|
5
5
|
require 'split/dashboard/helpers'
|
6
|
+
require 'split/dashboard/pagination_helpers'
|
6
7
|
|
7
8
|
module Split
|
8
9
|
class Dashboard < Sinatra::Base
|
@@ -14,6 +15,7 @@ module Split
|
|
14
15
|
set :method_override, true
|
15
16
|
|
16
17
|
helpers Split::DashboardHelpers
|
18
|
+
helpers Split::DashboardPaginationHelpers
|
17
19
|
|
18
20
|
get '/' do
|
19
21
|
# Display experiments without a winner at the top of the dashboard
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'split/dashboard/paginator'
|
3
|
+
|
4
|
+
module Split
|
5
|
+
module DashboardPaginationHelpers
|
6
|
+
DEFAULT_PER = 10
|
7
|
+
|
8
|
+
def pagination_per
|
9
|
+
@pagination_per ||= (params[:per] || DEFAULT_PER).to_i
|
10
|
+
end
|
11
|
+
|
12
|
+
def page_number
|
13
|
+
@page_number ||= (params[:page] || 1).to_i
|
14
|
+
end
|
15
|
+
|
16
|
+
def paginated(collection)
|
17
|
+
Split::DashboardPaginator.new(collection, page_number, pagination_per).paginate
|
18
|
+
end
|
19
|
+
|
20
|
+
def pagination(collection)
|
21
|
+
html = []
|
22
|
+
html << first_page_tag if show_first_page_tag?
|
23
|
+
html << ellipsis_tag if show_first_ellipsis_tag?
|
24
|
+
html << prev_page_tag if show_prev_page_tag?
|
25
|
+
html << current_page_tag
|
26
|
+
html << next_page_tag if show_next_page_tag?(collection)
|
27
|
+
html << ellipsis_tag if show_last_ellipsis_tag?(collection)
|
28
|
+
html << last_page_tagcollection if show_last_page_tag?(collection)
|
29
|
+
html.join
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def show_first_page_tag?
|
35
|
+
page_number > 2
|
36
|
+
end
|
37
|
+
|
38
|
+
def first_page_tag
|
39
|
+
%Q(<a href="#{url.chop}?page=1&per=#{pagination_per}">1</a>)
|
40
|
+
end
|
41
|
+
|
42
|
+
def show_first_ellipsis_tag?
|
43
|
+
page_number >= 4
|
44
|
+
end
|
45
|
+
|
46
|
+
def ellipsis_tag
|
47
|
+
'<span>...</span>'
|
48
|
+
end
|
49
|
+
|
50
|
+
def show_prev_page_tag?
|
51
|
+
page_number > 1
|
52
|
+
end
|
53
|
+
|
54
|
+
def prev_page_tag
|
55
|
+
%Q(<a href="#{url.chop}?page=#{page_number - 1}&per=#{pagination_per}">#{page_number - 1}</a>)
|
56
|
+
end
|
57
|
+
|
58
|
+
def current_page_tag
|
59
|
+
"<span><b>#{page_number}</b></span>"
|
60
|
+
end
|
61
|
+
|
62
|
+
def show_next_page_tag?(collection)
|
63
|
+
(page_number * pagination_per) < collection.count
|
64
|
+
end
|
65
|
+
|
66
|
+
def next_page_tag
|
67
|
+
%Q(<a href="#{url.chop}?page=#{page_number + 1}&per=#{pagination_per}">#{page_number + 1}</a>)
|
68
|
+
end
|
69
|
+
|
70
|
+
def show_last_ellipsis_tag?(collection)
|
71
|
+
(total_pages(collection) - page_number) >= 3
|
72
|
+
end
|
73
|
+
|
74
|
+
def total_pages(collection)
|
75
|
+
collection.count / pagination_per + ((collection.count % pagination_per).zero? ? 0 : 1)
|
76
|
+
end
|
77
|
+
|
78
|
+
def show_last_page_tag?(collection)
|
79
|
+
page_number < (total_pages(collection) - 1)
|
80
|
+
end
|
81
|
+
|
82
|
+
def last_page_tag(collection)
|
83
|
+
total = total_pages(collection)
|
84
|
+
%Q(<a href="#{url.chop}?page=#{total}&per=#{pagination_per}">#{total}</a>)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Split
|
3
|
+
class DashboardPaginator
|
4
|
+
def initialize(collection, page_number, per)
|
5
|
+
@collection = collection
|
6
|
+
@page_number = page_number
|
7
|
+
@per = per
|
8
|
+
end
|
9
|
+
|
10
|
+
def paginate
|
11
|
+
to = @page_number * @per
|
12
|
+
from = to - @per
|
13
|
+
@collection[from...to]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -317,3 +317,12 @@ a.button.green:focus, button.green:focus, input[type="submit"].green:focus {
|
|
317
317
|
}
|
318
318
|
|
319
319
|
|
320
|
+
.pagination {
|
321
|
+
text-align: center;
|
322
|
+
font-size: 15px;
|
323
|
+
}
|
324
|
+
|
325
|
+
.pagination a, .paginaton span {
|
326
|
+
display: inline-block;
|
327
|
+
padding: 5px;
|
328
|
+
}
|
@@ -6,7 +6,7 @@
|
|
6
6
|
<input type="button" id="toggle-active" value="Hide active" />
|
7
7
|
<input type="button" id="clear-filter" value="Clear filters" />
|
8
8
|
|
9
|
-
<% @experiments.each do |experiment| %>
|
9
|
+
<% paginated(@experiments).each do |experiment| %>
|
10
10
|
<% if experiment.goals.empty? %>
|
11
11
|
<%= erb :_experiment, :locals => {:goal => nil, :experiment => experiment} %>
|
12
12
|
<% else %>
|
@@ -16,6 +16,10 @@
|
|
16
16
|
<% end %>
|
17
17
|
<% end %>
|
18
18
|
<% end %>
|
19
|
+
|
20
|
+
<div class="pagination">
|
21
|
+
<%= pagination(@experiments) %>
|
22
|
+
</div>
|
19
23
|
<% else %>
|
20
24
|
<p class="intro">No experiments have started yet, you need to define them in your code and introduce them to your users.</p>
|
21
25
|
<p class="intro">Check out the <a href='https://github.com/splitrb/split#readme'>Readme</a> for more help getting started.</p>
|
data/lib/split/helper.rb
CHANGED
@@ -122,13 +122,17 @@ module Split
|
|
122
122
|
end
|
123
123
|
|
124
124
|
def exclude_visitor?
|
125
|
-
|
125
|
+
instance_exec(request, &Split.configuration.ignore_filter) || is_ignored_ip_address? || is_robot? || is_preview?
|
126
126
|
end
|
127
127
|
|
128
128
|
def is_robot?
|
129
129
|
defined?(request) && request.user_agent =~ Split.configuration.robot_regex
|
130
130
|
end
|
131
131
|
|
132
|
+
def is_preview?
|
133
|
+
defined?(request) && defined?(request.headers) && request.headers['x-purpose'] == 'preview'
|
134
|
+
end
|
135
|
+
|
132
136
|
def is_ignored_ip_address?
|
133
137
|
return false if Split.configuration.ignore_ip_addresses.empty?
|
134
138
|
|
@@ -6,6 +6,7 @@ module Split
|
|
6
6
|
class CookieAdapter
|
7
7
|
|
8
8
|
def initialize(context)
|
9
|
+
@context = context
|
9
10
|
@request, @response = context.request, context.response
|
10
11
|
@cookies = @request.cookies
|
11
12
|
@expires = Time.now + cookie_length_config
|
@@ -30,13 +31,43 @@ module Split
|
|
30
31
|
private
|
31
32
|
|
32
33
|
def set_cookie(value = {})
|
33
|
-
|
34
|
+
cookie_key = :split.to_s
|
35
|
+
cookie_value = default_options.merge(value: JSON.generate(value))
|
36
|
+
if action_dispatch?
|
37
|
+
# The "send" is necessary when we call ab_test from the controller
|
38
|
+
# and thus @context is a rails controller, because then "cookies" is
|
39
|
+
# a private method.
|
40
|
+
@context.send(:cookies)[cookie_key] = cookie_value
|
41
|
+
else
|
42
|
+
set_cookie_via_rack(cookie_key, cookie_value)
|
43
|
+
end
|
34
44
|
end
|
35
45
|
|
36
46
|
def default_options
|
37
47
|
{ expires: @expires, path: '/' }
|
38
48
|
end
|
39
49
|
|
50
|
+
def set_cookie_via_rack(key, value)
|
51
|
+
delete_cookie_header!(@response.header, key, value)
|
52
|
+
Rack::Utils.set_cookie_header!(@response.header, key, value)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Use Rack::Utils#make_delete_cookie_header after Rack 2.0.0
|
56
|
+
def delete_cookie_header!(header, key, value)
|
57
|
+
cookie_header = header['Set-Cookie']
|
58
|
+
case cookie_header
|
59
|
+
when nil, ''
|
60
|
+
cookies = []
|
61
|
+
when String
|
62
|
+
cookies = cookie_header.split("\n")
|
63
|
+
when Array
|
64
|
+
cookies = cookie_header
|
65
|
+
end
|
66
|
+
|
67
|
+
cookies.reject! { |cookie| cookie =~ /\A#{Rack::Utils.escape(key)}=/ }
|
68
|
+
header['Set-Cookie'] = cookies.join("\n")
|
69
|
+
end
|
70
|
+
|
40
71
|
def hash
|
41
72
|
@hash ||= begin
|
42
73
|
if cookies = @cookies[:split.to_s]
|
@@ -55,6 +86,9 @@ module Split
|
|
55
86
|
Split.configuration.persistence_cookie_length
|
56
87
|
end
|
57
88
|
|
89
|
+
def action_dispatch?
|
90
|
+
defined?(Rails) && @response.is_a?(ActionDispatch::Response)
|
91
|
+
end
|
58
92
|
end
|
59
93
|
end
|
60
94
|
end
|
data/lib/split/version.rb
CHANGED
@@ -0,0 +1,198 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'split/dashboard/pagination_helpers'
|
3
|
+
|
4
|
+
describe Split::DashboardPaginationHelpers do
|
5
|
+
include Split::DashboardPaginationHelpers
|
6
|
+
|
7
|
+
let(:url) { '/split/' }
|
8
|
+
|
9
|
+
describe '#pagination_per' do
|
10
|
+
context 'when params empty' do
|
11
|
+
let(:params) { Hash[] }
|
12
|
+
|
13
|
+
it 'returns 10' do
|
14
|
+
expect(pagination_per).to eql 10
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'when params[:per] is 5' do
|
19
|
+
let(:params) { Hash[per: 5] }
|
20
|
+
|
21
|
+
it 'returns 5' do
|
22
|
+
expect(pagination_per).to eql 5
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#page_number' do
|
28
|
+
context 'when params empty' do
|
29
|
+
let(:params) { Hash[] }
|
30
|
+
|
31
|
+
it 'returns 1' do
|
32
|
+
expect(page_number).to eql 1
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'when params[:page] is "2"' do
|
37
|
+
let(:params) { Hash[page: '2'] }
|
38
|
+
|
39
|
+
it 'returns 2' do
|
40
|
+
expect(page_number).to eql 2
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe '#paginated' do
|
46
|
+
let(:collection) { (1..20).to_a }
|
47
|
+
let(:params) { Hash[per: '5', page: '3'] }
|
48
|
+
|
49
|
+
it { expect(paginated(collection)).to eql [11, 12, 13, 14, 15] }
|
50
|
+
end
|
51
|
+
|
52
|
+
describe '#show_first_page_tag?' do
|
53
|
+
context 'when page is 1' do
|
54
|
+
it { expect(show_first_page_tag?).to be false }
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'when page is 3' do
|
58
|
+
let(:params) { Hash[page: '3'] }
|
59
|
+
it { expect(show_first_page_tag?).to be true }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe '#first_page_tag' do
|
64
|
+
it { expect(first_page_tag).to eql '<a href="/split?page=1&per=10">1</a>' }
|
65
|
+
end
|
66
|
+
|
67
|
+
describe '#show_first_ellipsis_tag?' do
|
68
|
+
context 'when page is 1' do
|
69
|
+
it { expect(show_first_ellipsis_tag?).to be false }
|
70
|
+
end
|
71
|
+
|
72
|
+
context 'when page is 4' do
|
73
|
+
let(:params) { Hash[page: '4'] }
|
74
|
+
it { expect(show_first_ellipsis_tag?).to be true }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe '#ellipsis_tag' do
|
79
|
+
it { expect(ellipsis_tag).to eql '<span>...</span>' }
|
80
|
+
end
|
81
|
+
|
82
|
+
describe '#show_prev_page_tag?' do
|
83
|
+
context 'when page is 1' do
|
84
|
+
it { expect(show_prev_page_tag?).to be false }
|
85
|
+
end
|
86
|
+
|
87
|
+
context 'when page is 2' do
|
88
|
+
let(:params) { Hash[page: '2'] }
|
89
|
+
it { expect(show_prev_page_tag?).to be true }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe '#prev_page_tag' do
|
94
|
+
context 'when page is 2' do
|
95
|
+
let(:params) { Hash[page: '2'] }
|
96
|
+
|
97
|
+
it do
|
98
|
+
expect(prev_page_tag).to eql '<a href="/split?page=1&per=10">1</a>'
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context 'when page is 3' do
|
103
|
+
let(:params) { Hash[page: '3'] }
|
104
|
+
|
105
|
+
it do
|
106
|
+
expect(prev_page_tag).to eql '<a href="/split?page=2&per=10">2</a>'
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe '#show_prev_page_tag?' do
|
112
|
+
context 'when page is 1' do
|
113
|
+
it { expect(show_prev_page_tag?).to be false }
|
114
|
+
end
|
115
|
+
|
116
|
+
context 'when page is 2' do
|
117
|
+
let(:params) { Hash[page: '2'] }
|
118
|
+
it { expect(show_prev_page_tag?).to be true }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
describe '#current_page_tag' do
|
123
|
+
context 'when page is 1' do
|
124
|
+
let(:params) { Hash[page: '1'] }
|
125
|
+
it { expect(current_page_tag).to eql '<span><b>1</b></span>' }
|
126
|
+
end
|
127
|
+
|
128
|
+
context 'when page is 2' do
|
129
|
+
let(:params) { Hash[page: '2'] }
|
130
|
+
it { expect(current_page_tag).to eql '<span><b>2</b></span>' }
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
describe '#show_next_page_tag?' do
|
135
|
+
context 'when page is 2' do
|
136
|
+
let(:params) { Hash[page: '2'] }
|
137
|
+
|
138
|
+
context 'when collection length is 20' do
|
139
|
+
let(:collection) { (1..20).to_a }
|
140
|
+
it { expect(show_next_page_tag?(collection)).to be false }
|
141
|
+
end
|
142
|
+
|
143
|
+
context 'when collection length is 25' do
|
144
|
+
let(:collection) { (1..25).to_a }
|
145
|
+
it { expect(show_next_page_tag?(collection)).to be true }
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
describe '#next_page_tag' do
|
151
|
+
context 'when page is 1' do
|
152
|
+
let(:params) { Hash[page: '1'] }
|
153
|
+
it { expect(next_page_tag).to eql '<a href="/split?page=2&per=10">2</a>' }
|
154
|
+
end
|
155
|
+
|
156
|
+
context 'when page is 2' do
|
157
|
+
let(:params) { Hash[page: '2'] }
|
158
|
+
it { expect(next_page_tag).to eql '<a href="/split?page=3&per=10">3</a>' }
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
describe '#total_pages' do
|
163
|
+
context 'when collection length is 30' do
|
164
|
+
let(:collection) { (1..30).to_a }
|
165
|
+
it { expect(total_pages(collection)).to eql 3 }
|
166
|
+
end
|
167
|
+
|
168
|
+
context 'when collection length is 35' do
|
169
|
+
let(:collection) { (1..35).to_a }
|
170
|
+
it { expect(total_pages(collection)).to eql 4 }
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
describe '#show_last_ellipsis_tag?' do
|
175
|
+
let(:collection) { (1..30).to_a }
|
176
|
+
let(:params) { Hash[per: '5', page: '2'] }
|
177
|
+
it { expect(show_last_ellipsis_tag?(collection)).to be true }
|
178
|
+
end
|
179
|
+
|
180
|
+
describe '#show_last_page_tag?' do
|
181
|
+
let(:collection) { (1..30).to_a }
|
182
|
+
|
183
|
+
context 'when page is 5/6' do
|
184
|
+
let(:params) { Hash[per: '5', page: '5'] }
|
185
|
+
it { expect(show_last_page_tag?(collection)).to be false }
|
186
|
+
end
|
187
|
+
|
188
|
+
context 'when page is 4/6' do
|
189
|
+
let(:params) { Hash[per: '5', page: '4'] }
|
190
|
+
it { expect(show_last_page_tag?(collection)).to be true }
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
describe '#last_page_tag' do
|
195
|
+
let(:collection) { (1..30).to_a }
|
196
|
+
it { expect(last_page_tag(collection)).to eql '<a href="/split?page=3&per=10">3</a>' }
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'spec_helper'
|
3
|
+
require 'split/dashboard/paginator'
|
4
|
+
|
5
|
+
describe Split::DashboardPaginator do
|
6
|
+
context 'when collection is 1..20' do
|
7
|
+
let(:collection) { (1..20).to_a }
|
8
|
+
|
9
|
+
context 'when per 5 for page' do
|
10
|
+
let(:per) { 5 }
|
11
|
+
|
12
|
+
it 'when page number is 1 result is [1, 2, 3, 4, 5]' do
|
13
|
+
result = Split::DashboardPaginator.new(collection, 1, per).paginate
|
14
|
+
expect(result).to eql [1, 2, 3, 4, 5]
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'when page number is 2 result is [6, 7, 8, 9, 10]' do
|
18
|
+
result = Split::DashboardPaginator.new(collection, 2, per).paginate
|
19
|
+
expect(result).to eql [6, 7, 8, 9, 10]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'when per 10 for page' do
|
24
|
+
let(:per) { 10 }
|
25
|
+
|
26
|
+
it 'when page number is 1 result is [1..10]' do
|
27
|
+
result = Split::DashboardPaginator.new(collection, 1, per).paginate
|
28
|
+
expect(result).to eql [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'when page number is 2 result is [10..20]' do
|
32
|
+
result = Split::DashboardPaginator.new(collection, 2, per).paginate
|
33
|
+
expect(result).to eql [11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/spec/helper_spec.rb
CHANGED
@@ -699,6 +699,14 @@ describe Split::Helper do
|
|
699
699
|
end
|
700
700
|
end
|
701
701
|
|
702
|
+
describe 'when user is previewing' do
|
703
|
+
before(:each) do
|
704
|
+
@request = OpenStruct.new(headers: { 'x-purpose' => 'preview' })
|
705
|
+
end
|
706
|
+
|
707
|
+
it_behaves_like "a disabled test"
|
708
|
+
end
|
709
|
+
|
702
710
|
describe 'versioned experiments' do
|
703
711
|
it "should use version zero if no version is present" do
|
704
712
|
alternative_name = ab_test('link_color', 'blue', 'red')
|
@@ -3,41 +3,104 @@ require "spec_helper"
|
|
3
3
|
require 'rack/test'
|
4
4
|
|
5
5
|
describe Split::Persistence::CookieAdapter do
|
6
|
+
subject { described_class.new(context) }
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
shared_examples "sets cookies correctly" do
|
9
|
+
describe "#[] and #[]=" do
|
10
|
+
it "set and return the value for given key" do
|
11
|
+
subject["my_key"] = "my_value"
|
12
|
+
expect(subject["my_key"]).to eq("my_value")
|
13
|
+
end
|
12
14
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
15
|
+
it "handles invalid JSON" do
|
16
|
+
context.request.cookies[:split] = {
|
17
|
+
:value => '{"foo":2,',
|
18
|
+
:expires => Time.now
|
19
|
+
}
|
20
|
+
expect(subject["my_key"]).to be_nil
|
21
|
+
subject["my_key"] = "my_value"
|
22
|
+
expect(subject["my_key"]).to eq("my_value")
|
23
|
+
end
|
17
24
|
end
|
18
|
-
end
|
19
25
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
26
|
+
describe "#delete" do
|
27
|
+
it "should delete the given key" do
|
28
|
+
subject["my_key"] = "my_value"
|
29
|
+
subject.delete("my_key")
|
30
|
+
expect(subject["my_key"]).to be_nil
|
31
|
+
end
|
25
32
|
end
|
26
|
-
end
|
27
33
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
34
|
+
describe "#keys" do
|
35
|
+
it "should return an array of the session's stored keys" do
|
36
|
+
subject["my_key"] = "my_value"
|
37
|
+
subject["my_second_key"] = "my_second_value"
|
38
|
+
expect(subject.keys).to match(["my_key", "my_second_key"])
|
39
|
+
end
|
33
40
|
end
|
34
41
|
end
|
35
42
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
43
|
+
|
44
|
+
context "when using Rack" do
|
45
|
+
let(:env) { Rack::MockRequest.env_for("http://example.com:8080/") }
|
46
|
+
let(:request) { Rack::Request.new(env) }
|
47
|
+
let(:response) { Rack::MockResponse.new(200, {}, "") }
|
48
|
+
let(:context) { double(request: request, response: response, cookies: CookiesMock.new) }
|
49
|
+
|
50
|
+
include_examples "sets cookies correctly"
|
51
|
+
|
52
|
+
it "puts multiple experiments in a single cookie" do
|
53
|
+
subject["foo"] = "FOO"
|
54
|
+
subject["bar"] = "BAR"
|
55
|
+
expect(context.response.headers["Set-Cookie"]).to match(/\Asplit=%7B%22foo%22%3A%22FOO%22%2C%22bar%22%3A%22BAR%22%7D; path=\/; expires=[a-zA-Z]{3}, \d{2} [a-zA-Z]{3} \d{4} \d{2}:\d{2}:\d{2} -0000\Z/)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "ensure other added cookies are not overriden" do
|
59
|
+
context.response.set_cookie 'dummy', 'wow'
|
60
|
+
subject["foo"] = "FOO"
|
61
|
+
expect(context.response.headers["Set-Cookie"]).to include("dummy=wow")
|
62
|
+
expect(context.response.headers["Set-Cookie"]).to include("split=")
|
63
|
+
end
|
41
64
|
end
|
42
65
|
|
66
|
+
context "when @context is an ActionController::Base" do
|
67
|
+
before :context do
|
68
|
+
require "rails"
|
69
|
+
require "action_controller/railtie"
|
70
|
+
end
|
71
|
+
|
72
|
+
let(:context) do
|
73
|
+
controller = controller_class.new
|
74
|
+
if controller.respond_to?(:set_request!)
|
75
|
+
controller.set_request!(ActionDispatch::Request.new({}))
|
76
|
+
else # Before rails 5.0
|
77
|
+
controller.send(:"request=", ActionDispatch::Request.new({}))
|
78
|
+
end
|
79
|
+
|
80
|
+
response = ActionDispatch::Response.new(200, {}, '').tap do |res|
|
81
|
+
res.request = controller.request
|
82
|
+
end
|
83
|
+
|
84
|
+
if controller.respond_to?(:set_response!)
|
85
|
+
controller.set_response!(response)
|
86
|
+
else # Before rails 5.0
|
87
|
+
controller.send(:set_response!, response)
|
88
|
+
end
|
89
|
+
controller
|
90
|
+
end
|
91
|
+
|
92
|
+
let(:controller_class) { Class.new(ActionController::Base) }
|
93
|
+
|
94
|
+
include_examples "sets cookies correctly"
|
95
|
+
|
96
|
+
it "puts multiple experiments in a single cookie" do
|
97
|
+
subject["foo"] = "FOO"
|
98
|
+
subject["bar"] = "BAR"
|
99
|
+
expect(subject.keys).to eq(["foo", "bar"])
|
100
|
+
expect(subject["foo"]).to eq("FOO")
|
101
|
+
expect(subject["bar"]).to eq("BAR")
|
102
|
+
cookie_jar = context.request.env["action_dispatch.cookies"]
|
103
|
+
expect(cookie_jar['split']).to eq("{\"foo\":\"FOO\",\"bar\":\"BAR\"}")
|
104
|
+
end
|
105
|
+
end
|
43
106
|
end
|
data/spec/split_spec.rb
CHANGED
@@ -15,19 +15,19 @@ RSpec.describe Split do
|
|
15
15
|
Split.redis = 'redis://localhost:6379'
|
16
16
|
expect(Split.redis).to be_a(Redis)
|
17
17
|
|
18
|
-
client = Split.redis.
|
19
|
-
expect(client
|
20
|
-
expect(client
|
18
|
+
client = Split.redis.connection
|
19
|
+
expect(client[:host]).to eq("localhost")
|
20
|
+
expect(client[:port]).to eq(6379)
|
21
21
|
end
|
22
22
|
|
23
23
|
it 'accepts an options hash' do
|
24
24
|
Split.redis = {host: 'localhost', port: 6379, db: 12}
|
25
25
|
expect(Split.redis).to be_a(Redis)
|
26
26
|
|
27
|
-
client = Split.redis.
|
28
|
-
expect(client
|
29
|
-
expect(client
|
30
|
-
expect(client
|
27
|
+
client = Split.redis.connection
|
28
|
+
expect(client[:host]).to eq("localhost")
|
29
|
+
expect(client[:port]).to eq(6379)
|
30
|
+
expect(client[:db]).to eq(12)
|
31
31
|
end
|
32
32
|
|
33
33
|
it 'accepts a valid Redis instance' do
|
data/split.gemspec
CHANGED
@@ -35,10 +35,10 @@ Gem::Specification.new do |s|
|
|
35
35
|
s.add_dependency 'simple-random', '>= 0.9.3'
|
36
36
|
|
37
37
|
s.add_development_dependency 'bundler', '~> 1.14'
|
38
|
-
s.add_development_dependency 'simplecov', '~> 0.
|
38
|
+
s.add_development_dependency 'simplecov', '~> 0.15'
|
39
39
|
s.add_development_dependency 'rack-test', '~> 0.6'
|
40
40
|
s.add_development_dependency 'rake', '~> 12'
|
41
|
-
s.add_development_dependency 'rspec', '~> 3.
|
41
|
+
s.add_development_dependency 'rspec', '~> 3.7'
|
42
42
|
s.add_development_dependency 'pry', '~> 0.10'
|
43
|
-
s.add_development_dependency 'fakeredis', '~> 0.
|
43
|
+
s.add_development_dependency 'fakeredis', '~> 0.7'
|
44
44
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: split
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Nesbitt
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-08-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -72,14 +72,14 @@ dependencies:
|
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: '0.
|
75
|
+
version: '0.15'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: '0.
|
82
|
+
version: '0.15'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: rack-test
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -114,14 +114,14 @@ dependencies:
|
|
114
114
|
requirements:
|
115
115
|
- - "~>"
|
116
116
|
- !ruby/object:Gem::Version
|
117
|
-
version: '3.
|
117
|
+
version: '3.7'
|
118
118
|
type: :development
|
119
119
|
prerelease: false
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
121
121
|
requirements:
|
122
122
|
- - "~>"
|
123
123
|
- !ruby/object:Gem::Version
|
124
|
-
version: '3.
|
124
|
+
version: '3.7'
|
125
125
|
- !ruby/object:Gem::Dependency
|
126
126
|
name: pry
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
@@ -142,14 +142,14 @@ dependencies:
|
|
142
142
|
requirements:
|
143
143
|
- - "~>"
|
144
144
|
- !ruby/object:Gem::Version
|
145
|
-
version: 0.
|
145
|
+
version: '0.7'
|
146
146
|
type: :development
|
147
147
|
prerelease: false
|
148
148
|
version_requirements: !ruby/object:Gem::Requirement
|
149
149
|
requirements:
|
150
150
|
- - "~>"
|
151
151
|
- !ruby/object:Gem::Version
|
152
|
-
version: 0.
|
152
|
+
version: '0.7'
|
153
153
|
description:
|
154
154
|
email:
|
155
155
|
- andrewnez@gmail.com
|
@@ -175,6 +175,7 @@ files:
|
|
175
175
|
- gemfiles/4.2.gemfile
|
176
176
|
- gemfiles/5.0.gemfile
|
177
177
|
- gemfiles/5.1.gemfile
|
178
|
+
- gemfiles/5.2.gemfile
|
178
179
|
- lib/split.rb
|
179
180
|
- lib/split/algorithms/block_randomization.rb
|
180
181
|
- lib/split/algorithms/weighted_sample.rb
|
@@ -184,6 +185,8 @@ files:
|
|
184
185
|
- lib/split/configuration.rb
|
185
186
|
- lib/split/dashboard.rb
|
186
187
|
- lib/split/dashboard/helpers.rb
|
188
|
+
- lib/split/dashboard/pagination_helpers.rb
|
189
|
+
- lib/split/dashboard/paginator.rb
|
187
190
|
- lib/split/dashboard/public/dashboard-filtering.js
|
188
191
|
- lib/split/dashboard/public/dashboard.js
|
189
192
|
- lib/split/dashboard/public/jquery-1.11.1.min.js
|
@@ -219,6 +222,8 @@ files:
|
|
219
222
|
- spec/alternative_spec.rb
|
220
223
|
- spec/combined_experiments_helper_spec.rb
|
221
224
|
- spec/configuration_spec.rb
|
225
|
+
- spec/dashboard/pagination_helpers_spec.rb
|
226
|
+
- spec/dashboard/paginator_spec.rb
|
222
227
|
- spec/dashboard_helpers_spec.rb
|
223
228
|
- spec/dashboard_spec.rb
|
224
229
|
- spec/encapsulated_helper_spec.rb
|
@@ -265,7 +270,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
265
270
|
version: 2.0.0
|
266
271
|
requirements: []
|
267
272
|
rubyforge_project: split
|
268
|
-
rubygems_version: 2.
|
273
|
+
rubygems_version: 2.7.3
|
269
274
|
signing_key:
|
270
275
|
specification_version: 4
|
271
276
|
summary: Rack based split testing framework
|
@@ -276,6 +281,8 @@ test_files:
|
|
276
281
|
- spec/alternative_spec.rb
|
277
282
|
- spec/combined_experiments_helper_spec.rb
|
278
283
|
- spec/configuration_spec.rb
|
284
|
+
- spec/dashboard/pagination_helpers_spec.rb
|
285
|
+
- spec/dashboard/paginator_spec.rb
|
279
286
|
- spec/dashboard_helpers_spec.rb
|
280
287
|
- spec/dashboard_spec.rb
|
281
288
|
- spec/encapsulated_helper_spec.rb
|