active_admin_filter_extensions 1.0.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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +42 -0
  3. data/.gitignore +7 -0
  4. data/.rspec +3 -0
  5. data/Gemfile +22 -0
  6. data/LICENSE +21 -0
  7. data/README.md +357 -0
  8. data/Rakefile +18 -0
  9. data/active_admin_filter_extensions.gemspec +29 -0
  10. data/config/locales/en.yml +10 -0
  11. data/lib/active_admin_filter_extensions/data_access.rb +22 -0
  12. data/lib/active_admin_filter_extensions/filter_defaults.rb +327 -0
  13. data/lib/active_admin_filter_extensions/filter_options.rb +70 -0
  14. data/lib/active_admin_filter_extensions/filters_form.rb +47 -0
  15. data/lib/active_admin_filter_extensions/notice.rb +38 -0
  16. data/lib/active_admin_filter_extensions/policies.rb +539 -0
  17. data/lib/active_admin_filter_extensions/redirect.rb +82 -0
  18. data/lib/active_admin_filter_extensions/required_filters.rb +83 -0
  19. data/lib/active_admin_filter_extensions/search_keys.rb +71 -0
  20. data/lib/active_admin_filter_extensions/version.rb +5 -0
  21. data/lib/active_admin_filter_extensions.rb +46 -0
  22. data/spec/data_access_spec.rb +252 -0
  23. data/spec/integration/extensions_container_spec.rb +38 -0
  24. data/spec/integration/filter_defaults_spec.rb +229 -0
  25. data/spec/integration/numeric_builtins_spec.rb +104 -0
  26. data/spec/integration/on_invalid_spec.rb +174 -0
  27. data/spec/integration/policies_spec.rb +204 -0
  28. data/spec/integration/redirect_spec.rb +204 -0
  29. data/spec/integration/require_any_filter_spec.rb +132 -0
  30. data/spec/integration/required_spec.rb +155 -0
  31. data/spec/integration/structural_keys_spec.rb +77 -0
  32. data/spec/integration/window_spec.rb +208 -0
  33. data/spec/integration/with_spec.rb +175 -0
  34. data/spec/rails_helper.rb +62 -0
  35. data/spec/redirect_install_spec.rb +27 -0
  36. data/spec/spec_helper.rb +68 -0
  37. data/spec/support/capybara_driver.rb +12 -0
  38. data/spec/support/rails_template.rb +637 -0
  39. data/tasks/test.rake +32 -0
  40. metadata +103 -0
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_helper"
4
+ require "rack/utils"
5
+
6
+ # The sample app registers Post once per filter input type - see spec/support/rails_template.rb.
7
+ # Every default below selects `keep me` and rejects `drop me`, so one pair of records exercises
8
+ # all of them. Capybara follows the one canonicalizing redirect, so every example asserts the
9
+ # final page.
10
+ RSpec.describe "Filter default values", type: :feature do
11
+ before do
12
+ AdminUser.create!(email: "admin@example.com", default_status: "published")
13
+
14
+ Post.create!(title: "keep me", body: "keep body", status: "published",
15
+ position: 20, published_date: Date.new(2026, 6, 1), starred: true)
16
+ Post.create!(title: "drop me", body: "drop body", status: "draft",
17
+ position: 1, published_date: Date.new(2020, 1, 1), starred: false)
18
+ end
19
+
20
+ describe "every filter input type" do
21
+ {
22
+ "string, predicate in the Hash key" => "/admin/string_posts",
23
+ "string, predicate already in the filter name" => "/admin/scalar_posts",
24
+ "numeric" => "/admin/numeric_posts",
25
+ "date range" => "/admin/date_posts",
26
+ "select" => "/admin/select_posts",
27
+ "check boxes" => "/admin/check_boxes_posts",
28
+ "boolean" => "/admin/boolean_posts"
29
+ }.each do |input_type, path|
30
+ it "applies the declared default to a #{input_type} filter" do
31
+ visit path
32
+
33
+ expect(page).to have_content("keep me")
34
+ expect(page).to have_no_content("drop me")
35
+ end
36
+ end
37
+ end
38
+
39
+ # `Proc#[]` is `call`, so a Proc left in `:input_html` answers Formtastic's `[:multiple]`
40
+ # check with a truthy Hash and a select derives `_in` instead of `_eq` - filtering nothing,
41
+ # silently. The Proc is resolved first, the way the filters form resolves it.
42
+ it "resolves a Proc :input_html before asking the input" do
43
+ visit "/admin/proc_input_html_posts"
44
+
45
+ expect(page).to have_content("keep me")
46
+ expect(page).to have_no_content("drop me")
47
+ expect(page).to have_select("q[status_eq]", selected: "published")
48
+ end
49
+
50
+ describe "a value that cannot be placed" do
51
+ it "says so instead of guessing" do
52
+ expect { visit "/admin/ambiguous_posts" }
53
+ .to raise_error(ArgumentError, /filter :published_date declares a `default:`.*single value cannot say which to fill/m)
54
+ end
55
+
56
+ it "says so for an exclusive Range instead of widening it" do
57
+ # The inputs submit gteq and lteq, so an excluded end has no spelling: stored as lteq it
58
+ # searched the day the Range left out, and the redirect froze that bound into every
59
+ # copied URL. Every other Range ambiguity here raises; this one changed data quietly.
60
+ expect { visit "/admin/banana_exclusive_range_posts" }
61
+ .to raise_error(ArgumentError, /exclusive Range.*use an inclusive range/m)
62
+ end
63
+ end
64
+
65
+ # Filtering the collection is only half of it: the admin has to be able to see what the page
66
+ # decided on their behalf, and change it. Every input type is checked, because each renders
67
+ # its value differently and a default that filters invisibly is the thing to avoid.
68
+ describe "the value the admin sees in the form" do
69
+ it "shows it on a string filter" do
70
+ visit "/admin/string_posts"
71
+
72
+ expect(page).to have_field("q[title_cont]", with: "keep")
73
+ end
74
+
75
+ it "shows it on a string filter whose name carries the predicate" do
76
+ visit "/admin/scalar_posts"
77
+
78
+ expect(page).to have_field("q[title_cont]", with: "keep")
79
+ end
80
+
81
+ it "shows it on a select filter" do
82
+ visit "/admin/select_posts"
83
+
84
+ expect(page).to have_select("q[status_eq]", selected: "published")
85
+ end
86
+
87
+ it "shows it on a check boxes filter" do
88
+ visit "/admin/check_boxes_posts"
89
+
90
+ expect(page).to have_checked_field("q[status_in][]", with: "published")
91
+ expect(page).to have_unchecked_field("q[status_in][]", with: "draft")
92
+ end
93
+
94
+ it "shows it on a boolean filter" do
95
+ visit "/admin/boolean_posts"
96
+
97
+ expect(page).to have_select("q[starred_eq]", selected: "Yes")
98
+ end
99
+
100
+ it "shows it on both ends of a date range filter" do
101
+ visit "/admin/date_posts"
102
+
103
+ expect(page).to have_field("q[published_date_gteq]", with: "2026-01-01")
104
+ expect(page).to have_field("q[published_date_lteq]", with: "")
105
+ end
106
+
107
+ it "shows it on a numeric filter, with the predicate its Hash named selected" do
108
+ visit "/admin/numeric_posts"
109
+
110
+ expect(page).to have_field("q[position_gt]", with: "10")
111
+ end
112
+
113
+ it "shows the one read off the signed in admin" do
114
+ visit "/admin/admin_preference_posts"
115
+
116
+ expect(page).to have_select("q[status_eq]", selected: "published")
117
+ end
118
+
119
+ it "shows everything once the field is blanked and the form submitted" do
120
+ visit "/admin/string_posts"
121
+ fill_in "q[title_cont]", with: ""
122
+ click_button "Filter"
123
+
124
+ expect(page).to have_content("keep me")
125
+ expect(page).to have_content("drop me")
126
+ end
127
+
128
+ it "leaves a resource that declares no default alone" do
129
+ visit "/admin/plain_posts"
130
+
131
+ expect(page).to have_content("keep me")
132
+ expect(page).to have_content("drop me")
133
+ end
134
+ end
135
+
136
+ describe ":if and :unless" do
137
+ it "neither renders a denied filter nor lets it impose its default" do
138
+ visit "/admin/conditional_posts"
139
+
140
+ # `title` is denied by `:if`, `body` by `:unless`: their inputs are gone, and their
141
+ # defaults are not in force either - `drop me` is excluded by the `status` filter alone.
142
+ expect(page).to have_no_field("q[title_cont]")
143
+ expect(page).to have_no_field("q[body_cont]")
144
+ expect(page).to have_field("q[status_eq]")
145
+ end
146
+
147
+ it "renders the index when every filter of the resource is denied" do
148
+ visit "/admin/all_hidden_posts"
149
+
150
+ expect(page).to have_content("keep me")
151
+ expect(page).to have_content("drop me")
152
+ end
153
+
154
+ it "applies the default of the filter its condition allows" do
155
+ visit "/admin/conditional_posts"
156
+
157
+ expect(page).to have_content("keep me")
158
+ expect(page).to have_no_content("drop me")
159
+ end
160
+ end
161
+
162
+ describe "a default read off the signed in admin" do
163
+ it "filters on what the admin saved" do
164
+ visit "/admin/admin_preference_posts"
165
+
166
+ expect(page).to have_content("keep me")
167
+ expect(page).to have_no_content("drop me")
168
+ end
169
+
170
+ it "applies no filter when the admin saved nothing" do
171
+ AdminUser.update_all(default_status: nil)
172
+
173
+ visit "/admin/admin_preference_posts"
174
+
175
+ expect(page).to have_content("keep me")
176
+ expect(page).to have_content("drop me")
177
+ end
178
+ end
179
+
180
+ describe "default_filters_notice" do
181
+ it "tells the admin why the list is cut down" do
182
+ visit "/admin/colliding_notice_posts"
183
+
184
+ expect(page).to have_content("Showing published posts by default")
185
+ end
186
+
187
+ it "stays quiet once the admin has filtered for themselves" do
188
+ visit "/admin/colliding_notice_posts"
189
+ select "draft", from: "q[status_eq]"
190
+ click_button "Filter"
191
+
192
+ expect(page).to have_no_content("Showing published posts by default")
193
+ end
194
+ end
195
+
196
+ describe "requests that carry no q of their own" do
197
+ it "keeps the default while paging" do
198
+ visit "/admin/string_posts?page=1"
199
+
200
+ expect(page).to have_no_content("drop me")
201
+ end
202
+
203
+ # The download link is built from params, which carry no `q` while the index is showing its
204
+ # declared defaults, so the CSV request has to derive them again to match the screen.
205
+ it "exports the filtered collection to CSV, not the whole table" do
206
+ visit "/admin/string_posts"
207
+ click_link "CSV"
208
+
209
+ expect(page.body).to include("keep me")
210
+ expect(page.body).to_not include("drop me")
211
+ end
212
+ end
213
+
214
+ # Clear Filters strips `q[`, `page`, `utf8` and `commit` from the query string, which is the
215
+ # state a first visit is in, so it returns the page to its declared defaults rather than to an
216
+ # empty filter set.
217
+ it "returns to the defaults after Clear Filters", :js do
218
+ visit "/admin/string_posts"
219
+ fill_in "q[title_cont]", with: ""
220
+ click_button "Filter"
221
+ expect(page).to have_content("drop me")
222
+
223
+ click_link "Clear Filters"
224
+
225
+ expect(page).to have_content("keep me")
226
+ expect(page).to have_no_content("drop me")
227
+ end
228
+
229
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_helper"
4
+ require "rack/utils"
5
+
6
+ # The built-in numeric pair: `sanitize: :digits` strips a value to its digits, `validate:
7
+ # :integer` refuses what does not fit a PG int4 - the guard every inventoried app hand-rolled
8
+ # around its id/position filters.
9
+ RSpec.describe "built-in numeric policies", type: :feature do
10
+ before do
11
+ Post.create!(title: "keep me", body: "keep body", status: "published",
12
+ position: 20, published_date: Date.new(2026, 6, 1), starred: true)
13
+ Post.create!(title: "drop me", body: "drop body", status: "draft",
14
+ position: 1, published_date: Date.new(2020, 1, 1), starred: false)
15
+ end
16
+
17
+ def query_of(url)
18
+ Rack::Utils.parse_nested_query(URI.parse(url).query.to_s)
19
+ end
20
+
21
+ describe "validate: :integer" do
22
+ it "refuses a value that does not fit a PG int4" do
23
+ visit "/admin/integer_posts?q%5Bposition_eq%5D=2147483648"
24
+
25
+ expect(page).to have_content("Position eq filter has an invalid value.")
26
+ expect(URI.parse(page.current_url).query).to be_nil
27
+ end
28
+
29
+ it "lets the int4 maximum itself through" do
30
+ visit "/admin/integer_posts?q%5Bposition_eq%5D=2147483647"
31
+
32
+ expect(query_of(page.current_url)["q"]).to eq("position_eq" => "2147483647")
33
+ expect(page).to have_no_content("invalid value")
34
+ end
35
+ end
36
+
37
+ describe "a default through its own sanitizer" do
38
+ it "stores and says the sanitized value on every format" do
39
+ # Not only the HTML path (which would self-heal with an extra hop): the CSV runs
40
+ # filtering_params directly and must search what the sanitizer produced.
41
+ visit "/admin/digit_default_posts.csv"
42
+
43
+ expect(page.body).to include("keep me")
44
+ expect(page.body).not_to include("drop me")
45
+ end
46
+
47
+ it "sanitizes a non-String default the same way every format will see it" do
48
+ visit "/admin/signed_default_posts.csv"
49
+
50
+ expect(page.body).to include("keep me")
51
+ end
52
+
53
+ it "lands the sanitized default in the URL in one hop" do
54
+ visit "/admin/digit_default_posts"
55
+
56
+ expect(query_of(page.current_url)["q"]).to eq("position_eq" => "20")
57
+ expect(page).to have_content("keep me")
58
+ end
59
+ end
60
+
61
+ describe "sibling predicates" do
62
+ # The filter's own predicate dropdown submits _gt/_lt/_not_eq and friends; a policy
63
+ # guarding only the head key leaves the same column one dropdown click from unpoliced.
64
+ it "polices a sibling predicate the dropdown submits" do
65
+ visit "/admin/integer_posts?q%5Bposition_gt%5D=2147483648"
66
+
67
+ expect(page).to have_content("has an invalid value")
68
+ expect(URI.parse(page.current_url).query).to be_nil
69
+ end
70
+
71
+ it "refills the owning filter's default after a sibling reset" do
72
+ # The reset deletes the sibling key; the refill promise belongs to the FILTER - its
73
+ # declared default comes back even though the submitted key never carried one.
74
+ visit "/admin/guarded_posts?q%5Btitle_eq%5D=keep123&commit=Filter"
75
+
76
+ expect(query_of(page.current_url)["q"]).to eq("title_cont" => "keep")
77
+ expect(page).to have_content("keep me")
78
+ expect(page).to have_no_content("drop me")
79
+ end
80
+
81
+ it "sanitizes a sibling predicate too" do
82
+ visit "/admin/integer_posts?q%5Bposition_lt%5D=2%200a"
83
+
84
+ expect(query_of(page.current_url)["q"]).to eq("position_lt" => "20")
85
+ expect(page).to have_content("drop me")
86
+ expect(page).to have_no_content("keep me")
87
+ end
88
+ end
89
+
90
+ describe "sanitize: :digits" do
91
+ it "strips the value to its digits and searches that" do
92
+ visit "/admin/integer_posts?q%5Bposition_eq%5D=%202-0a"
93
+
94
+ expect(query_of(page.current_url)["q"]).to eq("position_eq" => "20")
95
+ expect(page).to have_content("keep me")
96
+ expect(page).to have_no_content("drop me")
97
+ end
98
+
99
+ it "raises for an unknown built-in sanitizer" do
100
+ expect { visit "/admin/banana_sanitize_builtin_posts?q%5Btitle_cont%5D=x" }
101
+ .to raise_error(ArgumentError, /unknown built-in sanitizer/)
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_helper"
4
+ require "rack/utils"
5
+
6
+ # `on_invalid:` names what a violated policy does. :reset (the default) is covered elsewhere;
7
+ # here the other two: :reject bounces back, :empty shows nothing but says why.
8
+ RSpec.describe "on_invalid policies", type: :feature do
9
+ before do
10
+ Post.create!(title: "keep me", body: "keep body", status: "published",
11
+ position: 20, published_date: Date.new(2026, 6, 1), starred: true)
12
+ Post.create!(title: "drop me", body: "drop body", status: "draft",
13
+ position: 1, published_date: Date.new(2020, 1, 1), starred: false)
14
+ end
15
+
16
+ def query_of(url)
17
+ Rack::Utils.parse_nested_query(URI.parse(url).query.to_s)
18
+ end
19
+
20
+ describe ":reject" do
21
+ it "flashes and bounces to the bare index" do
22
+ visit "/admin/reject_posts?q%5Bposition_eq%5D=abc"
23
+
24
+ expect(page).to have_content("Position filter has an invalid value.")
25
+ expect(URI.parse(page.current_url).query).to be_nil
26
+ expect(page).to have_content("keep me")
27
+ expect(page).to have_content("drop me")
28
+ end
29
+
30
+ it "bounces once even when several filters are violated" do
31
+ # redirect_back does not break Ruby loops; without a performed? guard the second
32
+ # violation double-renders into a 500 instead of the promised bounce.
33
+ visit "/admin/reject_posts?q%5Bposition_eq%5D=abc&q%5Btitle_cont%5D=keep123"
34
+
35
+ expect(URI.parse(page.current_url).query).to be_nil
36
+ expect(page).to have_content("filter has an invalid value.").or have_content("must not contain digits")
37
+ end
38
+
39
+ it "bounces to the bare index when back is the offending URL itself" do
40
+ # Browsers keep the original Referer across a 302: redirect_back would re-reject the
41
+ # same request forever, dying at the browser's redirect cap instead of showing the
42
+ # error.
43
+ offending = "/admin/reject_posts?q%5Bposition_eq%5D=abc"
44
+ page.driver.header("Referer", "http://www.example.com#{offending}")
45
+ visit offending
46
+
47
+ expect(URI.parse(page.current_url).query).to be_nil
48
+ expect(page).to have_content("Position filter has an invalid value.")
49
+ ensure
50
+ page.driver.header("Referer", nil)
51
+ end
52
+
53
+ it "degrades to :reset off the HTML path" do
54
+ # A CSV index is answered in one request everywhere else in the gem, a flash it cannot
55
+ # render would be lost, and url_for drops the format - so the export is bounded the
56
+ # :reset way instead of being bounced into an HTML page.
57
+ visit "/admin/reject_posts.csv?q%5Bposition_eq%5D=abc"
58
+
59
+ expect(page.response_headers["Content-Type"]).to include("text/csv")
60
+ expect(page.body).to include("keep me")
61
+ end
62
+
63
+ it "bounces once when value and window policies are violated together" do
64
+ visit "/admin/reject_window_posts?q%5Bpublished_date_gteq%5D=2020-01-01X&q%5Bpublished_date_lteq%5D=2026-01-01X"
65
+
66
+ expect(URI.parse(page.current_url).query).to be_nil
67
+ expect(page).to have_content("No X dates.")
68
+ end
69
+ end
70
+
71
+ describe ":empty" do
72
+ it "shows nothing, says why, and keeps the admin's input on screen" do
73
+ visit "/admin/empty_posts?q%5Btitle_cont%5D=keep123"
74
+
75
+ expect(page).to have_content("Title filter must not contain digits.")
76
+ expect(page).to have_no_content("keep me")
77
+ expect(page).to have_no_content("drop me")
78
+ expect(page).to have_field("q[title_cont]", with: "keep123")
79
+ expect(URI.parse(page.current_url).query).to eq("q%5Btitle_cont%5D=keep123")
80
+ end
81
+
82
+ it "survives the canonicalizing redirect when the sanitizer empties the value" do
83
+ # digits("abc") is "" - the blank would otherwise redirect away, and the next request,
84
+ # flagless, renders the whole table.
85
+ visit "/admin/empty_digits_posts?q%5Bposition_eq%5D=abc"
86
+
87
+ expect(URI.parse(page.current_url).query).to eq("q%5Bposition_eq%5D=abc")
88
+ expect(page).to have_content("filter has an invalid value")
89
+ # The admin's own input is preserved (not the sanitizer's ""): the URL carries it and
90
+ # the form re-renders it - as Ransack casts it for this integer attribute, so "0".
91
+ # The literal-text case is EmptyPost's string-attribute example above.
92
+ expect(page).to have_field("q[position_eq]", with: "0")
93
+ expect(page).to have_no_content("keep me")
94
+ expect(page).to have_no_content("drop me")
95
+ end
96
+
97
+ it "validates array elements emptied by the sanitizer and keeps a checkbox input" do
98
+ visit "/admin/empty_digits_array_posts"
99
+ check "Not a number"
100
+ click_button "Filter"
101
+
102
+ expect(page).to have_content("filter has an invalid value")
103
+ expect(query_of(page.current_url)["q"]).to eq("status_in" => ["abc"])
104
+ expect(page).to have_checked_field("q[status_in][]", with: "abc")
105
+ expect(page).to have_no_content("keep me")
106
+ expect(page).to have_no_content("drop me")
107
+ end
108
+
109
+ it "leaves a valid search alone" do
110
+ visit "/admin/empty_posts?q%5Btitle_cont%5D=drop"
111
+
112
+ expect(page).to have_content("drop me")
113
+ expect(page).to have_no_content("keep me")
114
+ end
115
+ end
116
+
117
+ describe "a default against its own value policy" do
118
+ it "raises instead of flashing itself forever" do
119
+ # Accepted, the default refills on every bare visit, fails its own validator, resets,
120
+ # and refills - a permanent self-inflicted error banner.
121
+ expect { visit "/admin/banana_validated_default_posts" }
122
+ .to raise_error(ArgumentError, /default that violates its own validate/)
123
+ end
124
+ end
125
+
126
+ describe "a default against its own value policy" do
127
+ it "raises for a non-String default its validator refuses as submitted" do
128
+ # The URL will say the default as a String - that is the value to judge.
129
+ expect { visit "/admin/banana_integer_default_posts" }
130
+ .to raise_error(ArgumentError, /default that violates its own validate/)
131
+ end
132
+ end
133
+
134
+ describe "values the URL cannot carry" do
135
+ it "raises for a sanitizer with no fixpoint instead of redirect-looping" do
136
+ expect { visit "/admin/banana_sanitize_posts?q%5Btitle_cont%5D=acme" }
137
+ .to raise_error(ArgumentError, /sanitize.*idempotent/m)
138
+ end
139
+
140
+ it "raises for a default its sanitizer keeps rewriting" do
141
+ # The default faces the same fixpoint check submitted values do - otherwise the CSV
142
+ # searches the once-sanitized value while the HTML converges on another.
143
+ expect { visit "/admin/banana_shrinking_default_posts" }
144
+ .to raise_error(ArgumentError, /sanitize.*idempotent/m)
145
+ end
146
+
147
+ it "raises for a sanitizer that mutates the value in place" do
148
+ # `value << "%"` returns the SAME object - compared against itself, the no-fixpoint
149
+ # guard would wave the loop through.
150
+ expect { visit "/admin/banana_mutating_sanitize_posts?q%5Btitle_cont%5D=acme" }
151
+ .to raise_error(ArgumentError, /sanitize.*idempotent/m)
152
+ end
153
+
154
+ it "does not chase a default that serializes to nothing" do
155
+ # {}.to_query drops the pair entirely - same loop shape as the empty Array.
156
+ visit "/admin/empty_hash_default_posts"
157
+
158
+ expect(URI.parse(page.current_url).query).to be_nil
159
+ expect(page).to have_content("keep me")
160
+ end
161
+ end
162
+
163
+ describe "sanitize: rewrites the URL to what actually ran" do
164
+ it "redirects once to the sanitized value, without greeting it as a default" do
165
+ visit "/admin/guarded_posts?q%5Btitle_cont%5D=ke-ep"
166
+
167
+ expect(query_of(page.current_url)["q"]).to eq("title_cont" => "keep")
168
+ expect(page).to have_content("keep me")
169
+ # The search ran the admin's own (sanitized) value - no default engaged, so the
170
+ # travelling notice must stay home.
171
+ expect(page).to have_no_content("Showing kept posts by default")
172
+ end
173
+ end
174
+ end