geared_pagination 0.1 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +2 -1
- data/Gemfile.lock +34 -5
- data/README.md +29 -7
- data/geared_pagination.gemspec +1 -1
- data/lib/geared_pagination/controller.rb +6 -1
- data/lib/geared_pagination/headers.rb +1 -1
- data/lib/geared_pagination/page.rb +10 -5
- data/lib/geared_pagination/portion.rb +5 -0
- data/lib/geared_pagination/railtie.rb +1 -1
- data/lib/geared_pagination/ratios.rb +4 -0
- data/lib/geared_pagination/recordset.rb +1 -1
- data/test/controller_test.rb +66 -0
- data/test/headers_test.rb +12 -12
- data/test/page_test.rb +19 -0
- data/test/portion_test.rb +11 -0
- data/test/ratios_test.rb +4 -0
- data/test/recordset_test.rb +12 -12
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f2ef2f3f5dc638b5cd4b6ab2459cbf4e513b85e4
|
4
|
+
data.tar.gz: 3ae73c3c52ee2dd1a257daff921cabe401373733
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a66259e261f8f9b98459cbd754039fbfb304fd8c700181b3babb4dca4ceeb83efb2e49dbf1e00d1238ede83f1ddbc9d96812cdf603c5e0644bd2d59969fab21a
|
7
|
+
data.tar.gz: ba3b678d00e7615b1c8595ca342bd2d9cd1f4dee75a2e20f5ab93f178caf19b25b6d2abd6af3a4eb26b8dcf798ecee69a21d231fd4637e22a33f3fe9ce1087aa
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -8,27 +8,56 @@ PATH
|
|
8
8
|
GEM
|
9
9
|
remote: https://rubygems.org/
|
10
10
|
specs:
|
11
|
-
|
11
|
+
actionpack (5.0.2)
|
12
|
+
actionview (= 5.0.2)
|
13
|
+
activesupport (= 5.0.2)
|
14
|
+
rack (~> 2.0)
|
15
|
+
rack-test (~> 0.6.3)
|
16
|
+
rails-dom-testing (~> 2.0)
|
17
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
18
|
+
actionview (5.0.2)
|
19
|
+
activesupport (= 5.0.2)
|
20
|
+
builder (~> 3.1)
|
21
|
+
erubis (~> 2.7.0)
|
22
|
+
rails-dom-testing (~> 2.0)
|
23
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
24
|
+
activesupport (5.0.2)
|
12
25
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
13
26
|
i18n (~> 0.7)
|
14
27
|
minitest (~> 5.1)
|
15
28
|
tzinfo (~> 1.1)
|
16
|
-
addressable (2.5.
|
29
|
+
addressable (2.5.1)
|
17
30
|
public_suffix (~> 2.0, >= 2.0.2)
|
31
|
+
builder (3.2.3)
|
18
32
|
byebug (9.0.6)
|
19
|
-
concurrent-ruby (1.0.
|
33
|
+
concurrent-ruby (1.0.5)
|
34
|
+
erubis (2.7.0)
|
20
35
|
i18n (0.8.1)
|
36
|
+
loofah (2.0.3)
|
37
|
+
nokogiri (>= 1.5.9)
|
38
|
+
mini_portile2 (2.1.0)
|
21
39
|
minitest (5.10.1)
|
40
|
+
nokogiri (1.7.1)
|
41
|
+
mini_portile2 (~> 2.1.0)
|
22
42
|
public_suffix (2.0.5)
|
43
|
+
rack (2.0.1)
|
44
|
+
rack-test (0.6.3)
|
45
|
+
rack (>= 1.0)
|
46
|
+
rails-dom-testing (2.0.2)
|
47
|
+
activesupport (>= 4.2.0, < 6.0)
|
48
|
+
nokogiri (~> 1.6)
|
49
|
+
rails-html-sanitizer (1.0.3)
|
50
|
+
loofah (~> 2.0)
|
23
51
|
rake (12.0.0)
|
24
|
-
thread_safe (0.3.
|
25
|
-
tzinfo (1.2.
|
52
|
+
thread_safe (0.3.6)
|
53
|
+
tzinfo (1.2.3)
|
26
54
|
thread_safe (~> 0.1)
|
27
55
|
|
28
56
|
PLATFORMS
|
29
57
|
ruby
|
30
58
|
|
31
59
|
DEPENDENCIES
|
60
|
+
actionpack (>= 5)
|
32
61
|
bundler (~> 1.12)
|
33
62
|
byebug
|
34
63
|
geared_pagination!
|
data/README.md
CHANGED
@@ -1,30 +1,30 @@
|
|
1
1
|
# Geared Pagination
|
2
2
|
|
3
3
|
Most pagination schemes use a fixed page size. Page 1 returns as many elements as page 2. But that's
|
4
|
-
frequently not the most sensible way to page through a large
|
4
|
+
frequently not the most sensible way to page through a large recordset when you care about serving the
|
5
5
|
initial request as quickly as possible. This is particularly the case when using the pagination scheme
|
6
6
|
in combination with an infinite scrolling UI.
|
7
7
|
|
8
8
|
Geared Pagination allows you to define different ratios. By default, we will return 15 elements on page 1,
|
9
|
-
30 on page 2, 50 on page 3, and 100 from page 4 and forward. This has
|
9
|
+
30 on page 2, 50 on page 3, and 100 from page 4 and forward. This has proven to be a very sensible set of
|
10
10
|
ratios for much of the Basecamp UIs. But you can of course tweak the ratios, use fewer, or even none at all,
|
11
|
-
if
|
11
|
+
if a certain page calls for a fixed-rate scheme.
|
12
12
|
|
13
|
-
On
|
14
|
-
to be able to page through a
|
13
|
+
On JSON actions that set a page, we'll also automatically set Link and X-Total-Count headers for APIs
|
14
|
+
to be able to page through a recordset.
|
15
15
|
|
16
16
|
## Example
|
17
17
|
|
18
18
|
```ruby
|
19
19
|
class MessagesController < ApplicationController
|
20
20
|
def index
|
21
|
-
|
21
|
+
set_page_and_extract_portion_from Message.order(created_at: :desc)
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
25
|
# app/views/messages/index.html.erb
|
26
26
|
|
27
|
-
Showing page <%= @page.number %> of <%= @page.
|
27
|
+
Showing page <%= @page.number %> of <%= @page.recordset.page_count %> (<%= @page.recordset.records_count %> total messages):
|
28
28
|
|
29
29
|
<%= render @page.records %>
|
30
30
|
|
@@ -36,5 +36,27 @@ Showing page <%= @page.number %> of <%= @page.collection.page_count %> (<%= @pag
|
|
36
36
|
|
37
37
|
```
|
38
38
|
|
39
|
+
|
40
|
+
## Caching
|
41
|
+
|
42
|
+
To account for the current page in fragment caches, include the `@page` directly.
|
43
|
+
That includes the current page number and gear ratios.
|
44
|
+
|
45
|
+
Fragment caching a message's comments:
|
46
|
+
```ruby
|
47
|
+
<% cache [ @message, @page ] do %>
|
48
|
+
<%= render @page.records %>
|
49
|
+
<% end %>
|
50
|
+
```
|
51
|
+
|
52
|
+
NOTE: The page does not include cache keys for all the records. That would require loading all the records,
|
53
|
+
defeating the purpose of using the cache. Use a parent record, like a message that's touched when
|
54
|
+
new comments are posted, as the cache key instead.
|
55
|
+
|
56
|
+
## ETags
|
57
|
+
|
58
|
+
When a controller action sets an ETag and uses geared pagination, the current page and gear ratios are
|
59
|
+
automatically included in the ETag.
|
60
|
+
|
39
61
|
## License
|
40
62
|
Geared Pagination is released under the [MIT License](https://opensource.org/licenses/MIT).
|
data/geared_pagination.gemspec
CHANGED
@@ -7,6 +7,7 @@ module GearedPagination
|
|
7
7
|
|
8
8
|
included do
|
9
9
|
after_action :set_paginated_headers
|
10
|
+
etag { @page if geared_page? }
|
10
11
|
end
|
11
12
|
|
12
13
|
private
|
@@ -20,7 +21,11 @@ module GearedPagination
|
|
20
21
|
end
|
21
22
|
|
22
23
|
def set_paginated_headers
|
23
|
-
GearedPagination::Headers.new(page: @page, controller: self).apply if
|
24
|
+
GearedPagination::Headers.new(page: @page, controller: self).apply if geared_page?
|
25
|
+
end
|
26
|
+
|
27
|
+
def geared_page?
|
28
|
+
@page.is_a? GearedPagination::Page
|
24
29
|
end
|
25
30
|
|
26
31
|
def current_page_param
|
@@ -13,7 +13,7 @@ module GearedPagination
|
|
13
13
|
private
|
14
14
|
def headers
|
15
15
|
Hash.new.tap do |h|
|
16
|
-
h["X-Total-Count"] = @page.
|
16
|
+
h["X-Total-Count"] = @page.recordset.records_count.to_s
|
17
17
|
h["Link"] = next_page_link_header unless @page.last?
|
18
18
|
end
|
19
19
|
end
|
@@ -2,15 +2,15 @@ require 'geared_pagination/portion'
|
|
2
2
|
|
3
3
|
module GearedPagination
|
4
4
|
class Page
|
5
|
-
attr_reader :number, :
|
5
|
+
attr_reader :number, :recordset
|
6
6
|
|
7
7
|
def initialize(number, from:)
|
8
|
-
@number, @
|
8
|
+
@number, @recordset = number, from
|
9
9
|
@portion = GearedPagination::Portion.new(page_number: number, per_page: from.ratios)
|
10
10
|
end
|
11
11
|
|
12
12
|
def records
|
13
|
-
@records ||= @portion.from(
|
13
|
+
@records ||= @portion.from(recordset.records)
|
14
14
|
end
|
15
15
|
|
16
16
|
|
@@ -28,16 +28,21 @@ module GearedPagination
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def only?
|
31
|
-
|
31
|
+
recordset.page_count == 1
|
32
32
|
end
|
33
33
|
|
34
34
|
def last?
|
35
|
-
number ==
|
35
|
+
number == recordset.page_count
|
36
36
|
end
|
37
37
|
|
38
38
|
|
39
39
|
def next_number
|
40
40
|
number + 1
|
41
41
|
end
|
42
|
+
|
43
|
+
|
44
|
+
def cache_key
|
45
|
+
"page/#{@portion.cache_key}"
|
46
|
+
end
|
42
47
|
end
|
43
48
|
end
|
@@ -2,7 +2,7 @@ require 'rails/railtie'
|
|
2
2
|
require 'geared_pagination/controller'
|
3
3
|
|
4
4
|
class GearedPagination::Engine < ::Rails::Engine
|
5
|
-
initializer :
|
5
|
+
initializer :geared_pagination do |app|
|
6
6
|
ActiveSupport.on_load :action_controller do
|
7
7
|
ActionController::Base.send :include, GearedPagination::Controller
|
8
8
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'geared_pagination'
|
3
|
+
require 'geared_pagination/controller'
|
4
|
+
require 'active_support/all'
|
5
|
+
require 'action_controller'
|
6
|
+
|
7
|
+
class RecordingsController < ActionController::Base
|
8
|
+
include GearedPagination::Controller
|
9
|
+
|
10
|
+
def index
|
11
|
+
set_page_and_extract_portion_from Recording.all, per_page: params[:per_page]
|
12
|
+
render json: @page.records if stale? etag: "placeholder"
|
13
|
+
end
|
14
|
+
|
15
|
+
def unpaged
|
16
|
+
@page = "not a geared pagination page"
|
17
|
+
head :ok if stale? etag: "placeholder"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class GearedPagination::ControllerTest < ActionController::TestCase
|
22
|
+
tests RecordingsController
|
23
|
+
|
24
|
+
setup do
|
25
|
+
@routes = ActionDispatch::Routing::RouteSet.new.tap do |r|
|
26
|
+
r.draw do
|
27
|
+
resources :recordings do
|
28
|
+
get :unpaged, on: :collection
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
test "ETag includes the current page and gearing" do
|
35
|
+
get :index, params: { per_page: [ 1, 2 ] }
|
36
|
+
assert_equal etag_for("placeholder", "page/1:1-2"), response.etag
|
37
|
+
etag_before_gearing_change = response.etag
|
38
|
+
|
39
|
+
get :index, params: { page: 1, per_page: [ 1, 2 ] }
|
40
|
+
assert_equal etag_before_gearing_change, response.etag
|
41
|
+
|
42
|
+
get :index, params: { page: 1, per_page: [ 1, 3 ] }
|
43
|
+
assert_not_equal etag_before_gearing_change, response.etag
|
44
|
+
end
|
45
|
+
|
46
|
+
test "ETag is ignored when @page is not a geared page" do
|
47
|
+
get :unpaged
|
48
|
+
assert_equal etag_for("placeholder"), response.etag
|
49
|
+
end
|
50
|
+
|
51
|
+
test "Link headers on JSON requests" do
|
52
|
+
get :index, format: 'json'
|
53
|
+
assert_equal "120", response.headers["X-Total-Count"]
|
54
|
+
assert_equal '<http://test.host/recordings.json?page=2>; rel="next"', response.headers["Link"]
|
55
|
+
end
|
56
|
+
|
57
|
+
test "no Link headers on non-JSON requests" do
|
58
|
+
get :index
|
59
|
+
assert_nil response.headers["Link"]
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
def etag_for(*keys)
|
64
|
+
%(W/"#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(keys))}")
|
65
|
+
end
|
66
|
+
end
|
data/test/headers_test.rb
CHANGED
@@ -8,40 +8,40 @@ class GearedPagination::HeadersTest < ActiveSupport::TestCase
|
|
8
8
|
Controller = Struct.new(:request, :headers)
|
9
9
|
|
10
10
|
setup do
|
11
|
-
@controller_serving_json = Controller.new(Request.new("http://example.com/
|
12
|
-
@controller_serving_html = Controller.new(Request.new("http://example.com/
|
11
|
+
@controller_serving_json = Controller.new(Request.new("http://example.com/recordset.json", "json".inquiry), {})
|
12
|
+
@controller_serving_html = Controller.new(Request.new("http://example.com/recordset", "html".inquiry), {})
|
13
13
|
|
14
|
-
@
|
15
|
-
@
|
14
|
+
@single_page_recordset = GearedPagination::Recordset.new(Recording.all, per_page: 1000)
|
15
|
+
@many_page_recordset = GearedPagination::Recordset.new(Recording.all, per_page: 1)
|
16
16
|
end
|
17
17
|
|
18
18
|
test "total count" do
|
19
|
-
GearedPagination::Headers.new(page: @
|
19
|
+
GearedPagination::Headers.new(page: @many_page_recordset.page(1), controller: @controller_serving_json).apply
|
20
20
|
assert_equal Recording.all.count.to_s, @controller_serving_json.headers["X-Total-Count"]
|
21
21
|
end
|
22
22
|
|
23
23
|
test "no link for html requests" do
|
24
|
-
GearedPagination::Headers.new(page: @
|
24
|
+
GearedPagination::Headers.new(page: @many_page_recordset.page(1), controller: @controller_serving_html).apply
|
25
25
|
assert @controller_serving_html.headers["Link"].nil?
|
26
26
|
end
|
27
27
|
|
28
28
|
test "no link for json request with single page" do
|
29
|
-
GearedPagination::Headers.new(page: @
|
29
|
+
GearedPagination::Headers.new(page: @many_page_recordset.page(1), controller: @controller_serving_html).apply
|
30
30
|
assert @controller_serving_html.headers["Link"].nil?
|
31
31
|
end
|
32
32
|
|
33
33
|
test "links for json request with multiple pages" do
|
34
|
-
GearedPagination::Headers.new(page: @
|
35
|
-
assert_equal '<http://example.com/
|
34
|
+
GearedPagination::Headers.new(page: @many_page_recordset.page(1), controller: @controller_serving_json).apply
|
35
|
+
assert_equal '<http://example.com/recordset.json?page=2>; rel="next"',
|
36
36
|
@controller_serving_json.headers["Link"]
|
37
37
|
|
38
|
-
GearedPagination::Headers.new(page: @
|
39
|
-
assert_equal '<http://example.com/
|
38
|
+
GearedPagination::Headers.new(page: @many_page_recordset.page(2), controller: @controller_serving_json).apply
|
39
|
+
assert_equal '<http://example.com/recordset.json?page=3>; rel="next"',
|
40
40
|
@controller_serving_json.headers["Link"]
|
41
41
|
end
|
42
42
|
|
43
43
|
test "no link for json request with multiple pages on last page" do
|
44
|
-
GearedPagination::Headers.new(page: @
|
44
|
+
GearedPagination::Headers.new(page: @many_page_recordset.page(Recording.all.count), controller: @controller_serving_json).apply
|
45
45
|
assert @controller_serving_json.headers["Link"].nil?
|
46
46
|
end
|
47
47
|
end
|
data/test/page_test.rb
CHANGED
@@ -20,4 +20,23 @@ class GearedPagination::PageTest < ActiveSupport::TestCase
|
|
20
20
|
test "next_number" do
|
21
21
|
assert_equal 2, GearedPagination::Recordset.new(Recording.all, per_page: 1000).page(1).next_number
|
22
22
|
end
|
23
|
+
|
24
|
+
test "with empty recordset" do
|
25
|
+
page_for_empty_set = GearedPagination::Recordset.new(Recordings.new([]), per_page: 1000).page(1)
|
26
|
+
|
27
|
+
assert page_for_empty_set.first?
|
28
|
+
assert page_for_empty_set.only?
|
29
|
+
assert page_for_empty_set.last?
|
30
|
+
end
|
31
|
+
|
32
|
+
test "cache key changes according to current page and gearing" do
|
33
|
+
assert_equal 'page/2:3', cache_key(page: 2, per_page: 3)
|
34
|
+
assert_equal 'page/2:1-3', cache_key(page: 2, per_page: [ 1, 3 ])
|
35
|
+
assert_equal 'page/2:2-3', cache_key(page: 2, per_page: [ 2, 3 ])
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def cache_key(page:, per_page:)
|
40
|
+
GearedPagination::Recordset.new(Recording.all, per_page: per_page).page(page).cache_key
|
41
|
+
end
|
23
42
|
end
|
data/test/portion_test.rb
CHANGED
@@ -13,4 +13,15 @@ class GearedPagination::PortionTest < ActiveSupport::TestCase
|
|
13
13
|
assert_equal GearedPagination::Ratios::DEFAULTS.first, GearedPagination::Portion.new(page_number: 1).limit
|
14
14
|
assert_equal GearedPagination::Ratios::DEFAULTS.second, GearedPagination::Portion.new(page_number: 2).limit
|
15
15
|
end
|
16
|
+
|
17
|
+
test "cache key changes according to current page and gearing" do
|
18
|
+
assert_equal '2:3', cache_key(page: 2, per_page: 3)
|
19
|
+
assert_equal '2:1-3', cache_key(page: 2, per_page: [ 1, 3 ])
|
20
|
+
assert_equal '2:2-3', cache_key(page: 2, per_page: [ 2, 3 ])
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def cache_key(page:, per_page:)
|
25
|
+
GearedPagination::Portion.new(page_number: page, per_page: GearedPagination::Ratios.new(per_page)).cache_key
|
26
|
+
end
|
16
27
|
end
|
data/test/ratios_test.rb
CHANGED
@@ -24,4 +24,8 @@ class GearedPagination::RatiosTest < ActiveSupport::TestCase
|
|
24
24
|
assert_equal GearedPagination::Ratios::DEFAULTS.first, limits[1]
|
25
25
|
assert_equal GearedPagination::Ratios::DEFAULTS.last, limits[99]
|
26
26
|
end
|
27
|
+
|
28
|
+
test "cache key" do
|
29
|
+
assert_equal "1-2-3", GearedPagination::Ratios.new([ 1, 2, 3 ]).cache_key
|
30
|
+
end
|
27
31
|
end
|
data/test/recordset_test.rb
CHANGED
@@ -3,26 +3,26 @@ require 'geared_pagination/recordset'
|
|
3
3
|
|
4
4
|
class GearedPagination::RecordsetTest < ActiveSupport::TestCase
|
5
5
|
test "single limit pagination" do
|
6
|
-
|
6
|
+
recordset = GearedPagination::Recordset.new(Recording.all, per_page: 10)
|
7
7
|
|
8
|
-
assert_equal 10,
|
9
|
-
assert_equal 10,
|
8
|
+
assert_equal 10, recordset.page(1).records.size
|
9
|
+
assert_equal 10, recordset.page(2).records.size
|
10
10
|
end
|
11
11
|
|
12
12
|
test "variable limit pagination" do
|
13
|
-
|
13
|
+
recordset = GearedPagination::Recordset.new(Recording.all, per_page: [ 10, 15, 20 ])
|
14
14
|
|
15
|
-
assert_equal 10,
|
16
|
-
assert
|
15
|
+
assert_equal 10, recordset.page(1).records.size
|
16
|
+
assert recordset.page(1).records.include?(Recording.all[0])
|
17
17
|
|
18
|
-
assert_equal 15,
|
19
|
-
assert
|
18
|
+
assert_equal 15, recordset.page(2).records.size
|
19
|
+
assert recordset.page(2).records.include?(Recording.all[11])
|
20
20
|
|
21
|
-
assert_equal 20,
|
22
|
-
assert
|
21
|
+
assert_equal 20, recordset.page(3).records.size
|
22
|
+
assert recordset.page(3).records.include?(Recording.all[26])
|
23
23
|
|
24
|
-
assert_equal 20,
|
25
|
-
assert
|
24
|
+
assert_equal 20, recordset.page(4).records.size
|
25
|
+
assert recordset.page(4).records.include?(Recording.all[46])
|
26
26
|
end
|
27
27
|
|
28
28
|
test "page count" do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: geared_pagination
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.2'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Heinemeier Hansson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-05-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -72,6 +72,7 @@ files:
|
|
72
72
|
- lib/geared_pagination/railtie.rb
|
73
73
|
- lib/geared_pagination/ratios.rb
|
74
74
|
- lib/geared_pagination/recordset.rb
|
75
|
+
- test/controller_test.rb
|
75
76
|
- test/headers_test.rb
|
76
77
|
- test/page_test.rb
|
77
78
|
- test/portion_test.rb
|
@@ -104,6 +105,7 @@ signing_key:
|
|
104
105
|
specification_version: 4
|
105
106
|
summary: Paginate Active Record sets at variable speeds
|
106
107
|
test_files:
|
108
|
+
- test/controller_test.rb
|
107
109
|
- test/headers_test.rb
|
108
110
|
- test/page_test.rb
|
109
111
|
- test/portion_test.rb
|