Skip to content

Commit 7bc587e

Browse files
committed
feat: add tests for SidebarNavHelper rendering and caching behavior
1 parent 5edd3e5 commit 7bc587e

File tree

1 file changed

+363
-0
lines changed

1 file changed

+363
-0
lines changed
Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe BetterTogether::SidebarNavHelper do
6+
# Include engine routes for path helpers
7+
include BetterTogether::Engine.routes.url_helpers
8+
9+
let(:community) { @community }
10+
let(:nav) { @nav }
11+
let(:parent_page) { @parent_page }
12+
let(:child_page) { @child_page }
13+
let(:grandchild_page) { @grandchild_page }
14+
let(:current_page) { parent_page }
15+
16+
before do
17+
configure_host_platform
18+
@community = BetterTogether::Platform.host.first.community
19+
@nav = create(:better_together_navigation_area, navigable: @community)
20+
21+
# Define render_page_path helper for specs (it's a catch-all route)
22+
def helper.render_page_path(slug)
23+
"/#{slug}"
24+
end
25+
26+
# Create pages for linking (pages don't have community association)
27+
@parent_page = create(:better_together_page, slug: 'parent-page', protected: false)
28+
@child_page = create(:better_together_page, slug: 'child-page', protected: false)
29+
@grandchild_page = create(:better_together_page, slug: 'grandchild-page', protected: false)
30+
31+
# Clear cache before each test
32+
Rails.cache.clear
33+
end
34+
35+
describe '#render_sidebar_nav' do
36+
context 'with no navigation items' do
37+
it 'renders empty accordion structure' do
38+
result = helper.render_sidebar_nav(nav:, current_page:)
39+
40+
expect(result).to have_css('div.accordion#sidebar_nav_accordion')
41+
expect(result).not_to have_css('.accordion-item')
42+
end
43+
end
44+
45+
context 'with single top-level item' do
46+
let!(:nav_item) do
47+
create(:better_together_navigation_item,
48+
navigation_area: nav,
49+
linkable: parent_page,
50+
position: 1,
51+
parent_id: nil)
52+
end
53+
54+
it 'renders the navigation item' do
55+
result = helper.render_sidebar_nav(nav:, current_page:)
56+
57+
expect(result).to have_css('.accordion-item')
58+
expect(result).to have_link(nav_item.title)
59+
end
60+
61+
it 'marks current page as active' do
62+
result = helper.render_sidebar_nav(nav:, current_page: parent_page)
63+
64+
expect(result).to have_css('a.btn-sidebar-nav.active')
65+
end
66+
67+
it 'does not mark other pages as active' do
68+
result = helper.render_sidebar_nav(nav:, current_page: child_page)
69+
70+
expect(result).to have_css('a.btn-sidebar-nav.collapsed')
71+
expect(result).not_to have_css('a.btn-sidebar-nav.active')
72+
end
73+
end
74+
75+
context 'with hierarchical navigation items' do
76+
let!(:parent_nav_item) do
77+
create(:better_together_navigation_item,
78+
navigation_area: nav,
79+
linkable: parent_page,
80+
position: 1,
81+
parent_id: nil)
82+
end
83+
84+
let!(:child_nav_item) do
85+
create(:better_together_navigation_item,
86+
navigation_area: nav,
87+
linkable: child_page,
88+
position: 1,
89+
parent_id: parent_nav_item.id)
90+
end
91+
92+
let!(:grandchild_nav_item) do
93+
create(:better_together_navigation_item,
94+
navigation_area: nav,
95+
linkable: grandchild_page,
96+
position: 1,
97+
parent_id: child_nav_item.id)
98+
end
99+
100+
it 'renders nested accordion structure' do
101+
result = helper.render_sidebar_nav(nav:, current_page:)
102+
103+
expect(result).to have_css('.accordion-item.level-0')
104+
expect(result).to have_css('.accordion-item.level-1')
105+
expect(result).to have_css('.accordion-item.level-2')
106+
end
107+
108+
it 'includes collapse toggle for items with children' do
109+
result = helper.render_sidebar_nav(nav:, current_page:)
110+
111+
expect(result).to have_css('a.sidebar-level-toggle[data-bs-toggle="collapse"]')
112+
end
113+
114+
it 'expands parent when child is active' do
115+
result = helper.render_sidebar_nav(nav:, current_page: child_page)
116+
doc = Nokogiri::HTML(result)
117+
118+
collapse_div = doc.css("#collapse_#{parent_nav_item.id}").first
119+
expect(collapse_div['class']).to include('show')
120+
end
121+
122+
it 'expands ancestors when grandchild is active' do
123+
result = helper.render_sidebar_nav(nav:, current_page: grandchild_page)
124+
doc = Nokogiri::HTML(result)
125+
126+
parent_collapse = doc.css("#collapse_#{parent_nav_item.id}").first
127+
child_collapse = doc.css("#collapse_#{child_nav_item.id}").first
128+
129+
expect(parent_collapse['class']).to include('show')
130+
expect(child_collapse['class']).to include('show')
131+
end
132+
133+
it 'does not expand unrelated branches' do
134+
other_page = create(:better_together_page, slug: 'other-page')
135+
other_nav_item = create(:better_together_navigation_item,
136+
navigation_area: nav,
137+
linkable: other_page,
138+
position: 2,
139+
parent_id: nil)
140+
141+
result = helper.render_sidebar_nav(nav:, current_page: child_page)
142+
doc = Nokogiri::HTML(result)
143+
144+
other_collapse = doc.css("#collapse_#{other_nav_item.id}").first
145+
expect(other_collapse).to be_nil # No children, so no collapse div
146+
end
147+
end
148+
149+
context 'with navigation items without linkable' do
150+
let!(:nav_item_without_link) do
151+
create(:better_together_navigation_item,
152+
navigation_area: nav,
153+
linkable: nil,
154+
position: 1,
155+
parent_id: nil)
156+
end
157+
158+
it 'renders span instead of link' do
159+
result = helper.render_sidebar_nav(nav:, current_page:)
160+
161+
expect(result).to have_css('span.non-collapsible')
162+
expect(result).not_to have_link(nav_item_without_link.title)
163+
end
164+
end
165+
166+
context 'caching behavior' do
167+
let!(:nav_item) do
168+
create(:better_together_navigation_item,
169+
navigation_area: nav,
170+
linkable: parent_page,
171+
position: 1)
172+
end
173+
174+
it 'caches the rendered navigation' do
175+
# First call should cache
176+
first_result = helper.render_sidebar_nav(nav:, current_page:)
177+
178+
# Second call should use cache
179+
expect(Rails.cache).to receive(:fetch).and_call_original
180+
second_result = helper.render_sidebar_nav(nav:, current_page:)
181+
182+
expect(first_result).to eq(second_result)
183+
end
184+
185+
it 'uses different cache keys for different current pages' do
186+
result1 = helper.render_sidebar_nav(nav:, current_page: parent_page)
187+
result2 = helper.render_sidebar_nav(nav:, current_page: child_page)
188+
189+
# Results should be different because active states differ
190+
expect(result1).not_to eq(result2)
191+
end
192+
end
193+
194+
context 'with multiple positioned items' do
195+
let!(:first_item) do
196+
create(:better_together_navigation_item,
197+
navigation_area: nav,
198+
linkable: parent_page,
199+
position: 1,
200+
parent_id: nil)
201+
end
202+
203+
let!(:second_item) do
204+
create(:better_together_navigation_item,
205+
navigation_area: nav,
206+
linkable: child_page,
207+
position: 2,
208+
parent_id: nil)
209+
end
210+
211+
it 'renders items in position order' do
212+
result = helper.render_sidebar_nav(nav:, current_page:)
213+
doc = Nokogiri::HTML(result)
214+
215+
items = doc.css('.accordion-item.level-0')
216+
expect(items.count).to eq(2)
217+
end
218+
end
219+
end
220+
221+
describe '#render_nav_item' do
222+
let!(:nav_item) do
223+
create(:better_together_navigation_item,
224+
navigation_area: nav,
225+
linkable: parent_page,
226+
position: 1,
227+
parent_id: nil)
228+
end
229+
230+
before do
231+
# Set up instance variables that render_nav_item expects
232+
nav_items = nav.navigation_items.positioned.includes(:string_translations, linkable: %i[string_translations])
233+
helper.instance_variable_set(:@nav_item_cache, nav_items.index_by(&:id))
234+
helper.instance_variable_set(:@nav_item_children, nav_items.group_by(&:parent_id))
235+
end
236+
237+
it 'renders accordion item with correct level class' do
238+
result = helper.render_nav_item(
239+
nav_item:,
240+
current_page:,
241+
level: 0,
242+
parent_id: 'sidebar_nav_accordion',
243+
index: 0
244+
)
245+
246+
expect(result).to have_css('.accordion-item.level-0')
247+
end
248+
249+
it 'uses h3 for level 0' do
250+
result = helper.render_nav_item(
251+
nav_item:,
252+
current_page:,
253+
level: 0,
254+
parent_id: 'sidebar_nav_accordion',
255+
index: 0
256+
)
257+
258+
expect(result).to have_css('h3.accordion-header')
259+
end
260+
261+
it 'uses h4 for level 1' do
262+
child_nav_item = create(:better_together_navigation_item,
263+
navigation_area: nav,
264+
linkable: child_page,
265+
parent_id: nav_item.id,
266+
position: 1)
267+
268+
# Refresh instance variables
269+
nav_items = nav.navigation_items.positioned.includes(:string_translations, linkable: %i[string_translations])
270+
helper.instance_variable_set(:@nav_item_cache, nav_items.index_by(&:id))
271+
helper.instance_variable_set(:@nav_item_children, nav_items.group_by(&:parent_id))
272+
273+
result = helper.render_nav_item(
274+
nav_item: child_nav_item,
275+
current_page:,
276+
level: 1,
277+
parent_id: "collapse_#{nav_item.id}",
278+
index: 0
279+
)
280+
281+
expect(result).to have_css('h4.accordion-header')
282+
end
283+
284+
it 'limits heading level to h6 for deep nesting' do
285+
deep_item = create(:better_together_navigation_item,
286+
navigation_area: nav,
287+
linkable: parent_page,
288+
position: 1)
289+
290+
nav_items = nav.navigation_items.positioned.includes(:string_translations, linkable: %i[string_translations])
291+
helper.instance_variable_set(:@nav_item_cache, nav_items.index_by(&:id))
292+
helper.instance_variable_set(:@nav_item_children, nav_items.group_by(&:parent_id))
293+
294+
result = helper.render_nav_item(
295+
nav_item: deep_item,
296+
current_page:,
297+
level: 10,
298+
parent_id: 'parent',
299+
index: 0
300+
)
301+
302+
expect(result).to have_css('h6.accordion-header')
303+
end
304+
end
305+
306+
describe '#has_active_descendants?' do
307+
let!(:parent_nav_item) do
308+
create(:better_together_navigation_item,
309+
navigation_area: nav,
310+
linkable: parent_page,
311+
position: 1,
312+
parent_id: nil)
313+
end
314+
315+
let!(:child_nav_item) do
316+
create(:better_together_navigation_item,
317+
navigation_area: nav,
318+
linkable: child_page,
319+
position: 1,
320+
parent_id: parent_nav_item.id)
321+
end
322+
323+
before do
324+
nav_items = nav.navigation_items.positioned.includes(:string_translations, linkable: %i[string_translations])
325+
helper.instance_variable_set(:@nav_item_children, nav_items.group_by(&:parent_id))
326+
end
327+
328+
it 'returns true when child is the current page' do
329+
result = helper.has_active_descendants?(parent_nav_item.id, child_page)
330+
331+
expect(result).to be true
332+
end
333+
334+
it 'returns false when no descendants are active' do
335+
other_page = create(:better_together_page, slug: 'other')
336+
result = helper.has_active_descendants?(parent_nav_item.id, other_page)
337+
338+
expect(result).to be false
339+
end
340+
341+
it 'returns false when nav item has no children' do
342+
result = helper.has_active_descendants?(child_nav_item.id, parent_page)
343+
344+
expect(result).to be false
345+
end
346+
347+
it 'memoizes results for performance' do
348+
helper.has_active_descendants?(parent_nav_item.id, child_page)
349+
350+
cache = helper.instance_variable_get(:@active_descendant_cache)
351+
expect(cache).to have_key(parent_nav_item.id)
352+
end
353+
354+
it 'returns cached result on subsequent calls' do
355+
first_result = helper.has_active_descendants?(parent_nav_item.id, child_page)
356+
357+
# This should use cached value
358+
second_result = helper.has_active_descendants?(parent_nav_item.id, child_page)
359+
360+
expect(first_result).to eq(second_result)
361+
end
362+
end
363+
end

0 commit comments

Comments
 (0)