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.
- checksums.yaml +4 -4
- data/Gemfile +4 -3
- data/Gemfile.lock +58 -46
- data/README.md +2 -0
- data/app/controllers/admin/resource_controller.rb +1 -1
- data/app/controllers/application_controller.rb +15 -0
- data/app/models/asset.rb +29 -5
- data/app/models/standard_tags.rb +13 -7
- data/app/models/user.rb +13 -0
- data/lib/trusty_cms/site_scope_auth_reporter.rb +108 -0
- data/lib/trusty_cms/version.rb +1 -1
- data/package.json +1 -1
- data/spec/controllers/admin/snippets_controller_spec.rb +95 -0
- data/spec/controllers/site_controller_spec.rb +63 -0
- data/spec/factories/site.rb +8 -0
- data/spec/helpers/admin/references_helper_spec.rb +54 -0
- data/spec/helpers/admin/url_helper_spec.rb +71 -0
- data/spec/helpers/application_helper_spec.rb +102 -0
- data/spec/helpers/scoped_helper_spec.rb +60 -0
- data/spec/lib/active_record_extensions_spec.rb +65 -0
- data/spec/lib/login_system_spec.rb +61 -0
- data/spec/lib/ostruct_spec.rb +33 -0
- data/spec/lib/string_extensions_spec.rb +48 -0
- data/spec/lib/symbol_extensions_spec.rb +14 -0
- data/spec/lib/trusty_cms/config_cache_spec.rb +73 -0
- data/spec/models/admins_site_spec.rb +19 -0
- data/spec/models/asset_spec.rb +224 -0
- data/spec/models/asset_type_spec.rb +137 -0
- data/spec/models/file_not_found_page_spec.rb +33 -0
- data/spec/models/haml_filter_spec.rb +30 -0
- data/spec/models/menu_renderer_spec.rb +63 -0
- data/spec/models/page_attachment_spec.rb +29 -0
- data/spec/models/page_context_spec.rb +57 -0
- data/spec/models/page_field_spec.rb +15 -0
- data/spec/models/page_part_spec.rb +33 -0
- data/spec/models/page_spec.rb +184 -0
- data/spec/models/rails_page_spec.rb +32 -0
- data/spec/models/site_spec.rb +93 -0
- data/spec/models/snippet_finder_spec.rb +27 -0
- data/spec/models/snippet_tags_spec.rb +45 -0
- data/spec/models/standard_tags/children_spec.rb +159 -0
- data/spec/models/standard_tags/content_spec.rb +168 -0
- data/spec/models/standard_tags/meta_spec.rb +86 -0
- data/spec/models/standard_tags_spec.rb +32 -0
- data/spec/models/status_spec.rb +63 -0
- data/spec/models/text_filter_spec.rb +47 -0
- data/spec/models/trusty_cms/config_spec.rb +69 -0
- data/spec/models/trusty_cms/page_response_cache_director_spec.rb +72 -0
- data/spec/models/user_action_observer_spec.rb +39 -0
- data/spec/models/user_serialize_telemetry_spec.rb +124 -0
- data/spec/rails_helper.rb +13 -6
- data/spec/requests/admin/snippets_spec.rb +81 -0
- data/spec/spec_helper.rb +17 -3
- data/trusty_cms.gemspec +3 -3
- data/yarn.lock +273 -259
- 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
|