split 3.2.0 → 3.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.
- 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
|
[](https://codeclimate.com/github/splitrb/split)
|
6
6
|
[](https://codeclimate.com/github/splitrb/split/coverage)
|
7
7
|
[](https://github.com/RichardLitt/standard-readme)
|
8
|
+
[](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
|