trusty-cms 7.1.2 → 7.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +4 -3
  3. data/Gemfile.lock +58 -46
  4. data/README.md +2 -0
  5. data/app/controllers/admin/resource_controller.rb +1 -1
  6. data/app/controllers/application_controller.rb +15 -0
  7. data/app/models/asset.rb +29 -5
  8. data/app/models/standard_tags.rb +13 -7
  9. data/app/models/user.rb +13 -0
  10. data/lib/trusty_cms/site_scope_auth_reporter.rb +108 -0
  11. data/lib/trusty_cms/version.rb +1 -1
  12. data/package.json +1 -1
  13. data/spec/controllers/admin/snippets_controller_spec.rb +95 -0
  14. data/spec/controllers/site_controller_spec.rb +63 -0
  15. data/spec/factories/site.rb +8 -0
  16. data/spec/helpers/admin/references_helper_spec.rb +54 -0
  17. data/spec/helpers/admin/url_helper_spec.rb +71 -0
  18. data/spec/helpers/application_helper_spec.rb +102 -0
  19. data/spec/helpers/scoped_helper_spec.rb +60 -0
  20. data/spec/lib/active_record_extensions_spec.rb +65 -0
  21. data/spec/lib/login_system_spec.rb +61 -0
  22. data/spec/lib/ostruct_spec.rb +33 -0
  23. data/spec/lib/string_extensions_spec.rb +48 -0
  24. data/spec/lib/symbol_extensions_spec.rb +14 -0
  25. data/spec/lib/trusty_cms/config_cache_spec.rb +73 -0
  26. data/spec/models/admins_site_spec.rb +19 -0
  27. data/spec/models/asset_spec.rb +224 -0
  28. data/spec/models/asset_type_spec.rb +137 -0
  29. data/spec/models/file_not_found_page_spec.rb +33 -0
  30. data/spec/models/haml_filter_spec.rb +30 -0
  31. data/spec/models/menu_renderer_spec.rb +63 -0
  32. data/spec/models/page_attachment_spec.rb +29 -0
  33. data/spec/models/page_context_spec.rb +57 -0
  34. data/spec/models/page_field_spec.rb +15 -0
  35. data/spec/models/page_part_spec.rb +33 -0
  36. data/spec/models/page_spec.rb +184 -0
  37. data/spec/models/rails_page_spec.rb +32 -0
  38. data/spec/models/site_spec.rb +93 -0
  39. data/spec/models/snippet_finder_spec.rb +27 -0
  40. data/spec/models/snippet_tags_spec.rb +45 -0
  41. data/spec/models/standard_tags/children_spec.rb +159 -0
  42. data/spec/models/standard_tags/content_spec.rb +168 -0
  43. data/spec/models/standard_tags/meta_spec.rb +86 -0
  44. data/spec/models/standard_tags_spec.rb +32 -0
  45. data/spec/models/status_spec.rb +63 -0
  46. data/spec/models/text_filter_spec.rb +47 -0
  47. data/spec/models/trusty_cms/config_spec.rb +69 -0
  48. data/spec/models/trusty_cms/page_response_cache_director_spec.rb +72 -0
  49. data/spec/models/user_action_observer_spec.rb +39 -0
  50. data/spec/models/user_serialize_telemetry_spec.rb +124 -0
  51. data/spec/rails_helper.rb +13 -6
  52. data/spec/requests/admin/snippets_spec.rb +81 -0
  53. data/spec/spec_helper.rb +17 -3
  54. data/trusty_cms.gemspec +3 -3
  55. data/yarn.lock +273 -259
  56. metadata +91 -8
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ describe PagePart do
4
+ describe 'validations' do
5
+ it 'requires a name' do
6
+ part = PagePart.new(content: 'body content')
7
+ expect(part).not_to be_valid
8
+ expect(part.errors[:name]).to be_present
9
+ end
10
+
11
+ it 'rejects a name longer than 100 characters' do
12
+ part = PagePart.new(name: 'a' * 101)
13
+ expect(part).not_to be_valid
14
+ expect(part.errors[:name]).to be_present
15
+ end
16
+
17
+ it 'rejects a filter_id longer than 25 characters' do
18
+ part = PagePart.new(name: 'body', filter_id: 'a' * 26)
19
+ expect(part).not_to be_valid
20
+ expect(part.errors[:filter_id]).to be_present
21
+ end
22
+
23
+ it 'is valid with a name' do
24
+ expect(PagePart.new(name: 'body')).to be_valid
25
+ end
26
+ end
27
+
28
+ describe 'ordering' do
29
+ it 'orders by name via the default scope' do
30
+ expect(PagePart.all.to_sql).to match(/ORDER BY name/i)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,184 @@
1
+ require 'spec_helper'
2
+
3
+ describe Page do
4
+ let(:home) { FactoryBot.create(:home, title: 'Home') }
5
+
6
+ describe 'paths' do
7
+ it 'renders the root path as "/"' do
8
+ expect(home.path).to eq('/')
9
+ end
10
+
11
+ it 'builds a child path from the parent path and slug' do
12
+ parent = FactoryBot.create(:page, title: 'Parent', slug: 'parent', parent: home, status_id: Status[:published].id)
13
+ expect(parent.path).to eq('/parent')
14
+ end
15
+
16
+ it 'builds a nested path across multiple ancestors' do
17
+ parent = FactoryBot.create(:page, title: 'Parent', slug: 'parent', parent: home, status_id: Status[:published].id)
18
+ child = FactoryBot.create(:page, title: 'Child', slug: 'child', parent: parent, status_id: Status[:published].id)
19
+ expect(child.path).to eq('/parent/child')
20
+ end
21
+
22
+ it 'returns an empty string when the slug is blank' do
23
+ expect(FactoryBot.build(:page, slug: '').path).to eq('')
24
+ end
25
+
26
+ it '#child_path joins the page path with the child slug' do
27
+ parent = FactoryBot.create(:page, title: 'Parent', slug: 'parent', parent: home, status_id: Status[:published].id)
28
+ expect(home.child_path(parent)).to eq('/parent')
29
+ end
30
+
31
+ it 'aliases #url to #path' do
32
+ expect(home.url).to eq(home.path)
33
+ end
34
+ end
35
+
36
+ describe 'status' do
37
+ it '#status returns the Status for the status_id' do
38
+ expect(home.status).to eq(Status[:published])
39
+ expect(home.status.name).to eq('Published')
40
+ end
41
+
42
+ it '#status= assigns the status_id from a Status' do
43
+ page = FactoryBot.build(:page)
44
+ page.status = Status[:hidden]
45
+ expect(page.status_id).to eq(Status[:hidden].id)
46
+ end
47
+
48
+ it '#published? is true only for published pages' do
49
+ draft = FactoryBot.create(:page, title: 'Draft', slug: 'draft', parent: home, status_id: Status[:draft].id)
50
+ expect(home.published?).to be(true)
51
+ expect(draft.published?).to be(false)
52
+ end
53
+
54
+ it '#hidden? is true only for hidden pages' do
55
+ # Built (not created) to avoid the handle_hidden_status save callback,
56
+ # which references an unset config method in this environment.
57
+ hidden = FactoryBot.build(:page, title: 'Hidden', slug: 'hidden', parent: home, status_id: Status[:hidden].id)
58
+ expect(hidden.hidden?).to be(true)
59
+ expect(home.hidden?).to be(false)
60
+ end
61
+
62
+ it '#scheduled? is true only for scheduled pages' do
63
+ scheduled = FactoryBot.create(:page, title: 'Sched', slug: 'sched', parent: home, status_id: Status[:scheduled].id)
64
+ expect(scheduled.scheduled?).to be(true)
65
+ expect(home.scheduled?).to be(false)
66
+ end
67
+
68
+ it 'stamps published_at when a page is published' do
69
+ page = FactoryBot.create(:page, title: 'Pub', slug: 'pub', parent: home, status_id: Status[:published].id)
70
+ expect(page.published_at).to be_present
71
+ end
72
+
73
+ it 'does not stamp published_at for an unpublished page' do
74
+ draft = FactoryBot.create(:page, title: 'Draft2', slug: 'draft2', parent: home, status_id: Status[:draft].id)
75
+ expect(draft.published_at).to be_nil
76
+ end
77
+ end
78
+
79
+ describe 'parts' do
80
+ it '#part returns the named part' do
81
+ home.parts.create(name: 'body', content: 'x')
82
+ expect(home.part('body').content).to eq('x')
83
+ end
84
+
85
+ it '#part? reflects whether the part exists' do
86
+ home.parts.create(name: 'body', content: 'x')
87
+ expect(home.part?('body')).to be(true)
88
+ expect(home.part?('sidebar')).to be(false)
89
+ end
90
+
91
+ it '#inherits_part? is true when an ancestor has the part but the page does not' do
92
+ home.parts.create(name: 'body', content: 'x')
93
+ child = FactoryBot.create(:page, title: 'Child', slug: 'child', parent: home, status_id: Status[:published].id)
94
+ expect(child.part?('body')).to be(false)
95
+ expect(child.inherits_part?('body')).to be(true)
96
+ end
97
+
98
+ it '#has_or_inherits_part? covers both direct and inherited parts' do
99
+ home.parts.create(name: 'body', content: 'x')
100
+ child = FactoryBot.create(:page, title: 'Child', slug: 'child', parent: home, status_id: Status[:published].id)
101
+ expect(child.has_or_inherits_part?('body')).to be(true)
102
+ expect(child.has_or_inherits_part?('missing')).to be(false)
103
+ end
104
+ end
105
+
106
+ describe 'fields' do
107
+ it '#field looks up a field by name, case-insensitively' do
108
+ home.fields.create(name: 'Description', content: 'desc')
109
+ expect(home.field('description').content).to eq('desc')
110
+ expect(home.field(:Description).content).to eq('desc')
111
+ end
112
+
113
+ it '#field returns nil for an unknown field' do
114
+ expect(home.field('nope')).to be_nil
115
+ end
116
+ end
117
+
118
+ describe 'rendering' do
119
+ it '#render renders the body part when there is no layout' do
120
+ page = FactoryBot.create(:page, title: 'NoLayout', slug: 'nl', parent: home, status_id: Status[:published].id)
121
+ page.parts.create(name: 'body', content: 'Body: <r:title />')
122
+ expect(page.render).to eq('Body: NoLayout')
123
+ end
124
+
125
+ it '#render renders through the layout when one is present' do
126
+ layout = Layout.create!(name: 'Simple', content: 'Layout: <r:title />')
127
+ page = FactoryBot.create(:page, title: 'WithLayout', slug: 'wl', parent: home, status_id: Status[:published].id, layout: layout)
128
+ expect(page.render).to eq('Layout: WithLayout')
129
+ end
130
+
131
+ it '#render_part returns an empty string for a missing part' do
132
+ expect(home.render_part(:nonexistent)).to eq('')
133
+ end
134
+
135
+ it '#render_snippet parses the snippet content in the page context' do
136
+ snippet = FactoryBot.build(:snippet, name: 'greeting', content: 'Hello from <r:title />')
137
+ expect(home.render_snippet(snippet)).to eq('Hello from Home')
138
+ end
139
+ end
140
+
141
+ describe '.new_with_defaults' do
142
+ it 'builds a page with the configured default parts, fields, and status' do
143
+ page = Page.new_with_defaults
144
+ expect(page.parts.map(&:name)).to eq(%w[body extended])
145
+ expect(page.fields.map(&:name)).to eq(%w[Keywords Description])
146
+ expect(page.status).to eq(Status[:draft])
147
+ end
148
+ end
149
+
150
+ describe 'class helpers' do
151
+ it '.root returns the parentless page' do
152
+ home
153
+ expect(Page.root).to eq(home)
154
+ end
155
+
156
+ it '.date_column_names lists the date/time columns' do
157
+ expect(Page.date_column_names).to include('created_at', 'updated_at', 'published_at')
158
+ end
159
+
160
+ it '.descendant_class returns Page for blank/nil/"Page"' do
161
+ expect(Page.descendant_class(nil)).to eq(Page)
162
+ expect(Page.descendant_class('')).to eq(Page)
163
+ expect(Page.descendant_class('Page')).to eq(Page)
164
+ end
165
+
166
+ it '.descendant_class raises for an invalid class name' do
167
+ expect { Page.descendant_class('NotARealPage') }.to raise_error(ArgumentError)
168
+ end
169
+ end
170
+
171
+ describe 'response defaults' do
172
+ it '#cache? is true' do
173
+ expect(home.cache?).to be(true)
174
+ end
175
+
176
+ it '#response_code is 200' do
177
+ expect(home.response_code).to eq(200)
178
+ end
179
+
180
+ it '#headers is an empty hash' do
181
+ expect(home.headers).to eq({})
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,32 @@
1
+ require 'spec_helper'
2
+
3
+ describe RailsPage do
4
+ subject(:page) { RailsPage.new }
5
+
6
+ it 'is a Page' do
7
+ expect(page).to be_a(Page)
8
+ end
9
+
10
+ describe '#url' do
11
+ it 'returns an explicitly assigned url' do
12
+ page.url = '/custom/path'
13
+ expect(page.url).to eq('/custom/path')
14
+ end
15
+ end
16
+
17
+ describe '#build_parts_from_hash!' do
18
+ it 'creates page parts from a content hash' do
19
+ page.build_parts_from_hash!('body' => 'Hello', 'sidebar' => 'World')
20
+ expect(page.part('body').content).to eq('Hello')
21
+ expect(page.part('sidebar').content).to eq('World')
22
+ end
23
+
24
+ it 'updates the content of an existing part' do
25
+ page.build_parts_from_hash!('body' => 'first')
26
+ page.build_parts_from_hash!('body' => 'second')
27
+ bodies = page.parts.select { |p| p.name == 'body' }
28
+ expect(bodies.size).to eq(1)
29
+ expect(bodies.first.content).to eq('second')
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,93 @@
1
+ require 'spec_helper'
2
+
3
+ describe Site do
4
+ def create_site(attrs = {})
5
+ Site.create!({ name: 'Test', base_domain: 'test.local', domain: 'test\\.local' }.merge(attrs))
6
+ end
7
+
8
+ describe '#url' do
9
+ let(:site) { create_site(base_domain: 'example.com') }
10
+
11
+ it 'returns the site root for no path' do
12
+ expect(site.url).to eq('http://example.com/')
13
+ end
14
+
15
+ it 'joins a path onto the base domain' do
16
+ expect(site.url('/about')).to eq('http://example.com/about')
17
+ end
18
+ end
19
+
20
+ describe '.find_for_host' do
21
+ it 'returns the default site for a blank hostname' do
22
+ default = create_site(name: 'Default', base_domain: 'localhost', domain: '')
23
+ expect(Site.find_for_host('')).to eq(default)
24
+ end
25
+
26
+ it 'matches a site by its base_domain' do
27
+ # No empty-domain site here: an empty domain pattern matches every host
28
+ # (Regexp.compile('') matches anything) and would shadow specific sites.
29
+ blog = create_site(name: 'Blog', base_domain: 'blog.example.com', domain: 'blog\\.example\\.com')
30
+ expect(Site.find_for_host('blog.example.com')).to eq(blog)
31
+ end
32
+
33
+ it 'falls back to the default site for an unknown host' do
34
+ default = create_site(name: 'Default', base_domain: 'localhost', domain: '')
35
+ expect(Site.find_for_host('unknown.example.com')).to eq(default)
36
+ end
37
+ end
38
+
39
+ describe '.default' do
40
+ it 'returns the site whose domain is empty' do
41
+ default = create_site(name: 'Default', base_domain: 'localhost', domain: '')
42
+ expect(Site.default).to eq(default)
43
+ end
44
+ end
45
+
46
+ describe '.catchall' do
47
+ it 'creates a workable default site when none exists' do
48
+ site = Site.catchall
49
+ expect(site).to be_persisted
50
+ expect(site.name).to eq('default_site')
51
+ expect(site.base_domain).to eq('localhost')
52
+ end
53
+ end
54
+
55
+ describe '.several?' do
56
+ before { Site.several = nil }
57
+
58
+ it 'is false with a single site' do
59
+ create_site
60
+ expect(Site.several?).to be(false)
61
+ end
62
+
63
+ it 'is true with more than one site' do
64
+ create_site(name: 'One', base_domain: 'one.local', domain: 'one\\.local')
65
+ create_site(name: 'Two', base_domain: 'two.local', domain: 'two\\.local')
66
+ expect(Site.several?).to be(true)
67
+ end
68
+ end
69
+
70
+ describe 'validations' do
71
+ it 'requires a name and base_domain' do
72
+ site = Site.new
73
+ site.valid?
74
+ expect(site.errors[:name]).to be_present
75
+ expect(site.errors[:base_domain]).to be_present
76
+ end
77
+
78
+ it 'requires a unique domain' do
79
+ create_site(domain: 'dup\\.local')
80
+ dup = Site.new(name: 'Dup', base_domain: 'dup2.local', domain: 'dup\\.local')
81
+ expect(dup).not_to be_valid
82
+ expect(dup.errors[:domain]).to be_present
83
+ end
84
+ end
85
+
86
+ describe 'homepage creation' do
87
+ it 'builds a homepage automatically after create' do
88
+ site = create_site(base_domain: 'hp.local', domain: 'hp\\.local')
89
+ expect(site.homepage).to be_present
90
+ expect(site.homepage.breadcrumb).to eq('Home')
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ describe SnippetFinder do
4
+ describe '.finder_types' do
5
+ it 'looks in Snippet' do
6
+ expect(SnippetFinder.finder_types).to eq([Snippet])
7
+ end
8
+ end
9
+
10
+ describe '.find' do
11
+ it 'finds a snippet by id' do
12
+ snippet = FactoryBot.create(:snippet)
13
+ expect(SnippetFinder.find(snippet.id)).to eq(snippet)
14
+ end
15
+ end
16
+
17
+ describe '.find_by_name' do
18
+ it 'finds a snippet by name' do
19
+ snippet = FactoryBot.create(:snippet, name: 'sidebar')
20
+ expect(SnippetFinder.find_by_name('sidebar')).to eq(snippet)
21
+ end
22
+
23
+ it 'returns nil when no snippet matches' do
24
+ expect(SnippetFinder.find_by_name('does_not_exist')).to be_nil
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe SnippetTags do
4
+ # In production the snippets extension mixes SnippetTags in. Extend a single
5
+ # page instance (as MenuRenderer is used) so the snippet/yield tags are
6
+ # available through Page#parse without mutating the global Page class.
7
+ let(:page) { Page.new(title: 'Host').tap { |p| p.extend(SnippetTags) } }
8
+
9
+ def render(input)
10
+ page.send(:parse, input)
11
+ end
12
+
13
+ describe '<r:snippet>' do
14
+ it 'renders the named snippet within the page' do
15
+ FactoryBot.create(:snippet, name: 'hello', content: 'Hello Snippet')
16
+ expect(render('<r:snippet name="hello" />')).to eq('Hello Snippet')
17
+ end
18
+
19
+ it 'substitutes the double-tag body for <r:yield/>' do
20
+ FactoryBot.create(:snippet, name: 'wrap', content: 'before <r:yield/> after')
21
+ expect(render('<r:snippet name="wrap">MIDDLE</r:snippet>')).to eq('before MIDDLE after')
22
+ end
23
+
24
+ it 'raises when the name attribute is missing' do
25
+ expect { render('<r:snippet />') }.to raise_error(StandardTags::TagError, /name.*attribute/)
26
+ end
27
+
28
+ it 'raises when the snippet does not exist' do
29
+ expect { render('<r:snippet name="nope" />') }
30
+ .to raise_error(SnippetTags::TagError, /snippet 'nope' not found/)
31
+ end
32
+
33
+ it 'caches a snippet looked up more than once in a render' do
34
+ FactoryBot.create(:snippet, name: 'twice', content: 'X')
35
+ expect(SnippetFinder).to receive(:find_by_name).once.and_call_original
36
+ render('<r:snippet name="twice" /><r:snippet name="twice" />')
37
+ end
38
+ end
39
+
40
+ describe '<r:yield>' do
41
+ it 'outputs nothing outside of a double-tag snippet' do
42
+ expect(render('<r:yield/>')).to eq('')
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,159 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'StandardTags: children tags' do
4
+ def render(page, input)
5
+ page.send(:parse, input)
6
+ end
7
+
8
+ let(:home) { FactoryBot.create(:home) }
9
+
10
+ # Three published children (ascending published_at) plus one draft, so we can
11
+ # exercise ordering, status filtering, limit/offset and pagination.
12
+ before do
13
+ FactoryBot.create(:page, title: 'Apple', slug: 'apple', parent: home, status_id: Status[:published].id, published_at: Time.zone.local(2020, 1, 1))
14
+ FactoryBot.create(:page, title: 'Banana', slug: 'banana', parent: home, status_id: Status[:published].id, published_at: Time.zone.local(2020, 1, 2))
15
+ FactoryBot.create(:page, title: 'Cherry', slug: 'cherry', parent: home, status_id: Status[:published].id, published_at: Time.zone.local(2020, 1, 3))
16
+ FactoryBot.create(:page, title: 'Zeta', slug: 'zeta', parent: home, status_id: Status[:draft].id)
17
+ end
18
+
19
+ describe '<r:children>' do
20
+ it 'expands its contents' do
21
+ expect(render(home, '<r:children>hello</r:children>')).to eq('hello')
22
+ end
23
+ end
24
+
25
+ describe '<r:children:count>' do
26
+ it 'counts published children by default status' do
27
+ expect(render(home, '<r:children:count status="published" />')).to eq('3')
28
+ end
29
+
30
+ it 'counts all non-virtual children with status="all"' do
31
+ expect(render(home, '<r:children:count status="all" />')).to eq('4')
32
+ end
33
+ end
34
+
35
+ describe '<r:children:each>' do
36
+ it 'iterates published children ordered by published_at ascending' do
37
+ expect(render(home, '<r:children:each status="published"><r:title /> </r:children:each>'))
38
+ .to eq('Apple Banana Cherry ')
39
+ end
40
+
41
+ it 'orders by a given field and direction' do
42
+ expect(render(home, '<r:children:each status="published" by="title" order="desc"><r:title /> </r:children:each>'))
43
+ .to eq('Cherry Banana Apple ')
44
+ end
45
+
46
+ it 'honors the limit attribute' do
47
+ expect(render(home, '<r:children:each status="published" by="title" limit="2"><r:title /> </r:children:each>'))
48
+ .to eq('Apple Banana ')
49
+ end
50
+
51
+ it 'honors the offset attribute' do
52
+ expect(render(home, '<r:children:each status="published" by="title" offset="1"><r:title /> </r:children:each>'))
53
+ .to eq('Banana Cherry ')
54
+ end
55
+
56
+ it 'includes non-virtual draft pages with status="all"' do
57
+ expect(render(home, '<r:children:each status="all" by="title"><r:title /> </r:children:each>'))
58
+ .to eq('Apple Banana Cherry Zeta ')
59
+ end
60
+
61
+ it 'raises when the by attribute is not a valid field' do
62
+ expect { render(home, '<r:children:each status="published" by="bogus"><r:title /></r:children:each>') }
63
+ .to raise_error(StandardTags::TagError, /`by'/)
64
+ end
65
+
66
+ it 'raises when the order attribute is invalid' do
67
+ expect { render(home, '<r:children:each status="published" order="sideways"><r:title /></r:children:each>') }
68
+ .to raise_error(StandardTags::TagError, /`order'/)
69
+ end
70
+
71
+ it 'raises when the status attribute is not a valid status' do
72
+ expect { render(home, '<r:children:each status="bogus"><r:title /></r:children:each>') }
73
+ .to raise_error(StandardTags::TagError, /`status'/)
74
+ end
75
+
76
+ it 'raises when the status attribute is omitted' do
77
+ expect { render(home, '<r:children:each><r:title /></r:children:each>') }
78
+ .to raise_error(StandardTags::TagError, /`status'/)
79
+ end
80
+
81
+ it 'raises when limit is not a positive number' do
82
+ expect { render(home, '<r:children:each status="published" limit="x"><r:title /></r:children:each>') }
83
+ .to raise_error(StandardTags::TagError, /limit/)
84
+ end
85
+ end
86
+
87
+ describe 'child context tags inside <r:children:each>' do
88
+ it 'maps <r:child> attribute tags to the current child' do
89
+ expect(render(home, '<r:children:each status="published" by="title"><r:child><r:title /></r:child> </r:children:each>'))
90
+ .to eq('Apple Banana Cherry ')
91
+ end
92
+
93
+ it 'renders <r:if_first> only for the first child' do
94
+ expect(render(home, '<r:children:each status="published" by="title"><r:if_first>[first]</r:if_first><r:title /> </r:children:each>'))
95
+ .to eq('[first]Apple Banana Cherry ')
96
+ end
97
+
98
+ it 'renders <r:unless_first> for every child but the first' do
99
+ expect(render(home, '<r:children:each status="published" by="title"><r:unless_first>, </r:unless_first><r:title /></r:children:each>'))
100
+ .to eq('Apple, Banana, Cherry')
101
+ end
102
+
103
+ it 'renders <r:if_last> only for the last child' do
104
+ expect(render(home, '<r:children:each status="published" by="title"><r:title /><r:if_last>[last]</r:if_last> </r:children:each>'))
105
+ .to eq('Apple Banana Cherry[last] ')
106
+ end
107
+
108
+ it 'renders <r:unless_last> for every child but the last' do
109
+ expect(render(home, '<r:children:each status="published" by="title"><r:title /><r:unless_last>, </r:unless_last></r:children:each>'))
110
+ .to eq('Apple, Banana, Cherry')
111
+ end
112
+
113
+ it 'renders a header only when it changes' do
114
+ # A constant header renders once (before the first child) then is suppressed.
115
+ expect(render(home, '<r:children:each status="published" by="title"><r:header>H</r:header><r:title /> </r:children:each>'))
116
+ .to eq('HApple Banana Cherry ')
117
+ end
118
+ end
119
+
120
+ describe '<r:children:first> and <r:children:last>' do
121
+ it 'renders the first child (by published_at)' do
122
+ expect(render(home, '<r:children:first status="published"><r:title /></r:children:first>')).to eq('Apple')
123
+ end
124
+
125
+ it 'renders the last child (by published_at)' do
126
+ expect(render(home, '<r:children:last status="published"><r:title /></r:children:last>')).to eq('Cherry')
127
+ end
128
+
129
+ it 'respects ordering options when picking the first child' do
130
+ expect(render(home, '<r:children:first status="published" by="title" order="desc"><r:title /></r:children:first>')).to eq('Cherry')
131
+ end
132
+ end
133
+
134
+ describe 'pagination' do
135
+ # The pagination *controls* are rendered by the will_paginate view helper,
136
+ # which needs a view context that a model-level parse does not have, so we
137
+ # stub that layer and assert the results-slicing behavior of the tag.
138
+ before do
139
+ allow(home).to receive(:will_paginate).and_return('[controls]')
140
+ allow(home).to receive(:will_paginate_options).and_return({})
141
+ end
142
+
143
+ it 'renders only the current page of children plus pagination controls' do
144
+ home.pagination_parameters = { page: 1, per_page: 2 }
145
+ expect(render(home, '<r:children:each status="published" by="title" paginated="true" per_page="2"><r:title /> </r:children:each>'))
146
+ .to eq('Apple Banana [controls]')
147
+ end
148
+
149
+ it 'renders the second page of children' do
150
+ home.pagination_parameters = { page: 2, per_page: 2 }
151
+ expect(render(home, '<r:children:each status="published" by="title" paginated="true" per_page="2"><r:title /> </r:children:each>'))
152
+ .to eq('Cherry [controls]')
153
+ end
154
+
155
+ it 'renders nothing for the standalone pagination tag when there is no paginated list' do
156
+ expect(render(home, '<r:pagination />')).to eq('')
157
+ end
158
+ end
159
+ end