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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +42 -0
- data/.gitignore +7 -0
- data/.rspec +3 -0
- data/Gemfile +22 -0
- data/LICENSE +21 -0
- data/README.md +357 -0
- data/Rakefile +18 -0
- data/active_admin_filter_extensions.gemspec +29 -0
- data/config/locales/en.yml +10 -0
- data/lib/active_admin_filter_extensions/data_access.rb +22 -0
- data/lib/active_admin_filter_extensions/filter_defaults.rb +327 -0
- data/lib/active_admin_filter_extensions/filter_options.rb +70 -0
- data/lib/active_admin_filter_extensions/filters_form.rb +47 -0
- data/lib/active_admin_filter_extensions/notice.rb +38 -0
- data/lib/active_admin_filter_extensions/policies.rb +539 -0
- data/lib/active_admin_filter_extensions/redirect.rb +82 -0
- data/lib/active_admin_filter_extensions/required_filters.rb +83 -0
- data/lib/active_admin_filter_extensions/search_keys.rb +71 -0
- data/lib/active_admin_filter_extensions/version.rb +5 -0
- data/lib/active_admin_filter_extensions.rb +46 -0
- data/spec/data_access_spec.rb +252 -0
- data/spec/integration/extensions_container_spec.rb +38 -0
- data/spec/integration/filter_defaults_spec.rb +229 -0
- data/spec/integration/numeric_builtins_spec.rb +104 -0
- data/spec/integration/on_invalid_spec.rb +174 -0
- data/spec/integration/policies_spec.rb +204 -0
- data/spec/integration/redirect_spec.rb +204 -0
- data/spec/integration/require_any_filter_spec.rb +132 -0
- data/spec/integration/required_spec.rb +155 -0
- data/spec/integration/structural_keys_spec.rb +77 -0
- data/spec/integration/window_spec.rb +208 -0
- data/spec/integration/with_spec.rb +175 -0
- data/spec/rails_helper.rb +62 -0
- data/spec/redirect_install_spec.rb +27 -0
- data/spec/spec_helper.rb +68 -0
- data/spec/support/capybara_driver.rb +12 -0
- data/spec/support/rails_template.rb +637 -0
- data/tasks/test.rake +32 -0
- metadata +103 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails_helper"
|
|
4
|
+
require "rack/utils"
|
|
5
|
+
|
|
6
|
+
# `sanitize:` and `validate:` on a filter: value policies that used to live in hand-written
|
|
7
|
+
# before_actions. On violation the default policy is :reset - drop the offending key, flash
|
|
8
|
+
# the message, and let the declared default refill, with the redirect telling the truth about
|
|
9
|
+
# what actually ran.
|
|
10
|
+
RSpec.describe "Filter value policies", type: :feature do
|
|
11
|
+
before do
|
|
12
|
+
Post.create!(title: "keep me", body: "keep body", status: "published",
|
|
13
|
+
position: 20, published_date: Date.new(2026, 6, 1), starred: true)
|
|
14
|
+
Post.create!(title: "drop me", body: "drop body", status: "draft",
|
|
15
|
+
position: 1, published_date: Date.new(2020, 1, 1), starred: false)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def query_of(url)
|
|
19
|
+
Rack::Utils.parse_nested_query(URI.parse(url).query.to_s)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe "sanitize:" do
|
|
23
|
+
it "rewrites the value before the search sees it" do
|
|
24
|
+
visit "/admin/guarded_posts?q%5Btitle_cont%5D=ke-ep"
|
|
25
|
+
|
|
26
|
+
expect(page).to have_content("keep me")
|
|
27
|
+
expect(page).to have_no_content("drop me")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "sanitizes a frozen default without mutating the config" do
|
|
31
|
+
visit "/admin/frozen_default_posts"
|
|
32
|
+
|
|
33
|
+
expect(query_of(page.current_url)["q"]).to eq("title_cont" => "keep")
|
|
34
|
+
expect(page).to have_content("keep me")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe "validate: with the default :reset policy" do
|
|
39
|
+
it "drops the value, says why, and falls back to the declared default" do
|
|
40
|
+
visit "/admin/guarded_posts?q%5Btitle_cont%5D=keep123"
|
|
41
|
+
|
|
42
|
+
expect(page).to have_content("Title filter must not contain digits.")
|
|
43
|
+
# The default refilled - and the URL says so: validation ran before the redirect
|
|
44
|
+
# decision, so the address bar reflects what was actually searched.
|
|
45
|
+
expect(query_of(page.current_url)["q"]).to eq("title_cont" => "keep")
|
|
46
|
+
expect(page).to have_content("keep me")
|
|
47
|
+
expect(page).to have_no_content("drop me")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "does not greet the fallback with the defaults notice" do
|
|
51
|
+
# One render, one message: the error already explains why the page shows the
|
|
52
|
+
# default slice - the notice on top of it would be noise.
|
|
53
|
+
visit "/admin/guarded_posts?q%5Btitle_cont%5D=keep123"
|
|
54
|
+
|
|
55
|
+
expect(page).to have_no_content("Showing kept posts by default")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
it "refills the default on a real filters-form submission too" do
|
|
60
|
+
# The form always sends commit; the reset's whole promise is the refill, and a promise
|
|
61
|
+
# that holds only on hand-typed URLs is not one.
|
|
62
|
+
visit "/admin/guarded_posts?q%5Btitle_cont%5D=keep123&commit=Filter"
|
|
63
|
+
|
|
64
|
+
expect(page).to have_content("Title filter must not contain digits.")
|
|
65
|
+
expect(query_of(page.current_url)["q"]).to eq("title_cont" => "keep")
|
|
66
|
+
expect(page).to have_no_content("drop me")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
it "refuses a value that is not a plain string" do
|
|
71
|
+
# Ransack executes ?q[title_cont][value]=x byte-identically to the scalar - skipping
|
|
72
|
+
# the policy for non-String shapes was a straight bypass.
|
|
73
|
+
visit "/admin/guarded_posts?q%5Btitle_cont%5D%5Bvalue%5D=keep123"
|
|
74
|
+
|
|
75
|
+
expect(page).to have_content("filter has an invalid value")
|
|
76
|
+
expect(query_of(page.current_url)["q"]).to eq("title_cont" => "keep")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "leaves a valid value alone" do
|
|
80
|
+
visit "/admin/guarded_posts?q%5Btitle_cont%5D=drop"
|
|
81
|
+
|
|
82
|
+
expect(page).to have_no_content("Title filter must not contain digits.")
|
|
83
|
+
expect(query_of(page.current_url)["q"]).to eq("title_cont" => "drop")
|
|
84
|
+
expect(page).to have_content("drop me")
|
|
85
|
+
expect(page).to have_no_content("keep me")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "still greets a bare visit with the notice" do
|
|
89
|
+
visit "/admin/guarded_posts"
|
|
90
|
+
|
|
91
|
+
expect(page).to have_content("Showing kept posts by default")
|
|
92
|
+
expect(page).to have_no_content("drop me")
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
describe "a multi-valued filter" do
|
|
97
|
+
it "lets its Array submission through" do
|
|
98
|
+
visit "/admin/policed_boxes_posts?q%5Bstatus_in%5D%5B%5D=draft"
|
|
99
|
+
|
|
100
|
+
expect(page).to have_no_content("No x statuses.")
|
|
101
|
+
expect(page).to have_content("drop me")
|
|
102
|
+
expect(page).to have_no_content("keep me")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "judges each element and resets the key on a bad one" do
|
|
106
|
+
visit "/admin/policed_boxes_posts?q%5Bstatus_in%5D%5B%5D=draft&q%5Bstatus_in%5D%5B%5D=xdraft"
|
|
107
|
+
|
|
108
|
+
expect(page).to have_content("No x statuses.")
|
|
109
|
+
expect(page).to have_content("keep me")
|
|
110
|
+
expect(page).to have_no_content("drop me")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it "ignores a blank element beside a real selection" do
|
|
114
|
+
# ?q[status_in][]=&q[status_in][]=draft is the shape a multi-select submits, and
|
|
115
|
+
# normalization drops the blank before searching - judged, it flashed an error about a
|
|
116
|
+
# value nothing searches and reset a legitimate selection to the default.
|
|
117
|
+
visit "/admin/policed_boxes_posts?q%5Bstatus_in%5D%5B%5D=&q%5Bstatus_in%5D%5B%5D=draft"
|
|
118
|
+
|
|
119
|
+
expect(page).to have_no_content("Letters only.")
|
|
120
|
+
expect(page).to have_content("drop me")
|
|
121
|
+
expect(page).to have_no_content("keep me")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "removes blank elements before sanitizing a multi-valued filter" do
|
|
125
|
+
visit "/admin/integer_sanitized_boxes_posts?q%5Bposition_in%5D%5B%5D=&q%5Bposition_in%5D%5B%5D=20"
|
|
126
|
+
|
|
127
|
+
expect(page).to have_content("keep me")
|
|
128
|
+
expect(page).to have_no_content("drop me")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "still refuses a Hash-wrapped value on the same filter" do
|
|
132
|
+
visit "/admin/policed_boxes_posts?q%5Bstatus_in%5D%5Bvalue%5D=draft"
|
|
133
|
+
|
|
134
|
+
expect(page).to have_content("has an invalid value")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it "ignores a blank member of an Array default" do
|
|
138
|
+
# nil became "", the validator refused it, and materialization raised on every request -
|
|
139
|
+
# for exactly the admins whose per-admin member resolved to nothing.
|
|
140
|
+
visit "/admin/nil_member_policed_posts"
|
|
141
|
+
|
|
142
|
+
expect(query_of(page.current_url)["q"]).to eq("status_in" => ["published"])
|
|
143
|
+
expect(page).to have_content("keep me")
|
|
144
|
+
expect(page).to have_no_content("drop me")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it "materializes an Array default beside its validator" do
|
|
148
|
+
visit "/admin/policed_boxes_posts"
|
|
149
|
+
|
|
150
|
+
expect(page).to have_content("keep me")
|
|
151
|
+
expect(page).to have_no_content("drop me")
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
describe "message: overrides the violation text" do
|
|
156
|
+
it "speaks the filter's own words for a built-in validator" do
|
|
157
|
+
visit "/admin/ip_guarded_posts?q%5Btitle_cont%5D=not-an-ip"
|
|
158
|
+
|
|
159
|
+
expect(page).to have_content("Allowed IP address is incorrect!")
|
|
160
|
+
expect(page).to have_no_content("filter has an invalid value")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it "falls back to the generic message for a Symbol no locale defines" do
|
|
164
|
+
visit "/admin/banana_message_posts?q%5Btitle_cont%5D=keep123"
|
|
165
|
+
|
|
166
|
+
expect(page).to have_content("Title cont filter has an invalid value.")
|
|
167
|
+
expect(page).to have_no_content("translation missing")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it "interpolates the override" do
|
|
171
|
+
# Used verbatim, the app's own words could never name %{limit} or %{floor}.
|
|
172
|
+
visit "/admin/interpolated_message_posts?q%5Bpublished_date_gteq%5D=2020-01-01" \
|
|
173
|
+
"&q%5Bpublished_date_lteq%5D=2026-01-01"
|
|
174
|
+
|
|
175
|
+
expect(page).to have_content("Pick at most 90 days.")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it "does not refuse a blank default" do
|
|
179
|
+
# Blank is no value: the URL cannot carry it and Ransack would strip it, so judging it
|
|
180
|
+
# raised over a default that never reaches a search.
|
|
181
|
+
visit "/admin/blank_default_policed_posts"
|
|
182
|
+
|
|
183
|
+
expect(URI.parse(page.current_url).query).to be_nil
|
|
184
|
+
expect(page).to have_content("keep me")
|
|
185
|
+
expect(page).to have_content("drop me")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it "falls back to the generic text for a parameterized symbol it cannot interpolate" do
|
|
189
|
+
# The docs invite a validate: that returns a gem scope symbol; the parameterized ones
|
|
190
|
+
# have nothing to interpolate from, and raising there 500s the request the policy is
|
|
191
|
+
# meant to answer.
|
|
192
|
+
visit "/admin/parameterized_symbol_posts?q%5Btitle_cont%5D=keep123"
|
|
193
|
+
|
|
194
|
+
expect(page).to have_content("Title cont filter has an invalid value.")
|
|
195
|
+
expect(page).to have_no_content("translation missing")
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
it "stays quiet for a valid value" do
|
|
199
|
+
visit "/admin/ip_guarded_posts?q%5Btitle_cont%5D=10.0.0.0%2F8"
|
|
200
|
+
|
|
201
|
+
expect(page).to have_no_content("Allowed IP address is incorrect!")
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails_helper"
|
|
4
|
+
require "rack/utils"
|
|
5
|
+
|
|
6
|
+
# The gem's defining behavior: the bare index redirects once to itself with the effective
|
|
7
|
+
# filters spelled out in the query string. What the admin copies out of the address bar is
|
|
8
|
+
# then the page they were looking at - same window, same values - instead of a bare path that
|
|
9
|
+
# every recipient resolves against their own clock and their own preferences.
|
|
10
|
+
RSpec.describe "The redirect", type: :feature do
|
|
11
|
+
before do
|
|
12
|
+
Post.create!(title: "keep me", body: "keep body", status: "published",
|
|
13
|
+
position: 20, published_date: Date.new(2026, 6, 1), starred: true)
|
|
14
|
+
Post.create!(title: "drop me", body: "drop body", status: "draft",
|
|
15
|
+
position: 1, published_date: Date.new(2020, 1, 1), starred: false)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def query_of(url)
|
|
19
|
+
Rack::Utils.parse_nested_query(URI.parse(url).query.to_s)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe "a bare visit" do
|
|
23
|
+
it "lands on a URL that carries the defaults, values frozen" do
|
|
24
|
+
visit "/admin/posts"
|
|
25
|
+
|
|
26
|
+
expect(query_of(page.current_url)["q"])
|
|
27
|
+
.to eq("status_eq" => "published", "published_date_gteq" => "2026-01-01")
|
|
28
|
+
expect(page).to have_content("keep me")
|
|
29
|
+
expect(page).to have_no_content("drop me")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "keeps the rest of the query string" do
|
|
33
|
+
visit "/admin/posts?order=title_asc&page=1"
|
|
34
|
+
|
|
35
|
+
expect(query_of(page.current_url)).to include("order" => "title_asc", "page" => "1")
|
|
36
|
+
expect(query_of(page.current_url)["q"]).to include("status_eq" => "published")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe "a widened filter_defaults_apply?" do
|
|
41
|
+
it "adds the defaults beside the pinned key in one hop" do
|
|
42
|
+
visit "/admin/widened_posts?q%5Btitle_cont%5D=keep"
|
|
43
|
+
|
|
44
|
+
expect(query_of(page.current_url)["q"])
|
|
45
|
+
.to eq("title_cont" => "keep", "status_eq" => "published")
|
|
46
|
+
expect(page).to have_content("keep me")
|
|
47
|
+
expect(page).to have_no_content("drop me")
|
|
48
|
+
|
|
49
|
+
# The redirected-to URL itself passes through - the guard has converged.
|
|
50
|
+
converged = page.current_url
|
|
51
|
+
visit converged
|
|
52
|
+
expect(page.current_url).to eq(converged)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "treats `?q=` - an empty string - as a bare visit" do
|
|
57
|
+
visit "/admin/posts?q="
|
|
58
|
+
|
|
59
|
+
expect(query_of(page.current_url)["q"]).to include("status_eq" => "published")
|
|
60
|
+
expect(page).to have_no_content("drop me")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
describe "a request that already says what it wants" do
|
|
64
|
+
it "is left alone when it carries its own filters" do
|
|
65
|
+
visit "/admin/posts?q%5Bstatus_eq%5D=draft"
|
|
66
|
+
|
|
67
|
+
expect(query_of(page.current_url)["q"]).to eq("status_eq" => "draft")
|
|
68
|
+
expect(page).to have_content("drop me")
|
|
69
|
+
expect(page).to have_no_content("keep me")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "is left alone after a blank filters-form submission" do
|
|
73
|
+
# Submitting the form with every field blank sends `commit` and no `q` - the admin asked
|
|
74
|
+
# for everything, and a redirect back to the defaults would make that impossible to ask.
|
|
75
|
+
visit "/admin/posts?commit=Filter"
|
|
76
|
+
|
|
77
|
+
expect(query_of(page.current_url)).to eq("commit" => "Filter")
|
|
78
|
+
expect(page).to have_content("keep me")
|
|
79
|
+
expect(page).to have_content("drop me")
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The URL is only rewritten where an address bar is watching. The defaults still filter a
|
|
84
|
+
# CSV export - that is the gem's core, not the mode's - so skipping the redirect loses
|
|
85
|
+
# nothing but the detour.
|
|
86
|
+
it "exports CSV in one request, defaults still applied" do
|
|
87
|
+
visit "/admin/posts.csv"
|
|
88
|
+
|
|
89
|
+
expect(URI.parse(page.current_url).query).to be_nil
|
|
90
|
+
expect(page.body).to include("keep me")
|
|
91
|
+
expect(page.body).not_to include("drop me")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "does not chase a default the URL cannot carry" do
|
|
95
|
+
# to_query drops an empty Array entirely, so redirecting on it would loop: the redirected
|
|
96
|
+
# URL parses back without the key and the next request is bare again. Such a value stays
|
|
97
|
+
# out of the URL instead.
|
|
98
|
+
visit "/admin/empty_default_posts"
|
|
99
|
+
|
|
100
|
+
expect(URI.parse(page.current_url).query).to be_nil
|
|
101
|
+
expect(page).to have_content("keep me")
|
|
102
|
+
expect(page).to have_content("drop me")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "does not chase a blank default the URL cannot round-trip" do
|
|
106
|
+
# ?q[k]= comes back in with the blank stripped - chasing it alternates between the blank
|
|
107
|
+
# URL and the bare one forever.
|
|
108
|
+
visit "/admin/banana_blank_default_posts"
|
|
109
|
+
|
|
110
|
+
expect(URI.parse(page.current_url).query).to be_nil
|
|
111
|
+
expect(page).to have_content("keep me")
|
|
112
|
+
expect(page).to have_content("drop me")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "does not chase an Array default whose members are all blank" do
|
|
116
|
+
# [nil] survives a plain blank-reject, serializes to ?q[k][]= and parses back as [""] -
|
|
117
|
+
# which normalization drops, so the defaults re-apply and the two sides never agree.
|
|
118
|
+
visit "/admin/nil_member_default_posts"
|
|
119
|
+
|
|
120
|
+
expect(URI.parse(page.current_url).query).to be_nil
|
|
121
|
+
expect(page).to have_content("keep me")
|
|
122
|
+
expect(page).to have_content("drop me")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "does not redirect a resource that declares no defaults" do
|
|
126
|
+
visit "/admin/no_defaults_posts"
|
|
127
|
+
|
|
128
|
+
expect(URI.parse(page.current_url).query).to be_nil
|
|
129
|
+
expect(page).to have_content("keep me")
|
|
130
|
+
expect(page).to have_content("drop me")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# A batch action flashes and redirects back to the index; the bare index then answers 302
|
|
134
|
+
# again. Flash survives exactly one request, so without keeping it the extra hop eats every
|
|
135
|
+
# such message.
|
|
136
|
+
it "keeps a flash that arrived from another action across the extra hop" do
|
|
137
|
+
visit "/admin/posts/poke"
|
|
138
|
+
|
|
139
|
+
expect(query_of(page.current_url)["q"]).to include("status_eq" => "published")
|
|
140
|
+
expect(page).to have_content("poked")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Without this the redirect would silently eat every notice: the flash is set on the request
|
|
144
|
+
# the defaults apply to, and that is the one that answers 302 - so it has to travel.
|
|
145
|
+
describe "a declared default_filters_notice" do
|
|
146
|
+
it "survives the redirect" do
|
|
147
|
+
visit "/admin/notice_posts"
|
|
148
|
+
|
|
149
|
+
expect(query_of(page.current_url)["q"]).to eq("status_eq" => "published")
|
|
150
|
+
expect(page).to have_content("Showing published posts by default")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it "does not eat a flash that arrived from another action" do
|
|
154
|
+
# Writing the notice loads the flash, and a loaded flash sweeps what came in with the
|
|
155
|
+
# request when it commits - here, on a 302 that renders nothing. The redirect keeps the
|
|
156
|
+
# arriving entries, so a batch action's message and the notice land together.
|
|
157
|
+
visit "/admin/notice_posts/poke"
|
|
158
|
+
|
|
159
|
+
expect(query_of(page.current_url)["q"]).to eq("status_eq" => "published")
|
|
160
|
+
expect(page).to have_content("poked")
|
|
161
|
+
expect(page).to have_content("Showing published posts by default")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it "yields its shared flash key to a message that arrived from another action" do
|
|
165
|
+
# On the default :notice key the notice and a batch action's message collide; the
|
|
166
|
+
# arriving one names what just happened, so it is the one the admin must see.
|
|
167
|
+
visit "/admin/colliding_notice_posts/poke"
|
|
168
|
+
|
|
169
|
+
expect(page).to have_content("poked")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it "still greets a bare visit on its default flash key" do
|
|
173
|
+
visit "/admin/colliding_notice_posts"
|
|
174
|
+
|
|
175
|
+
expect(page).to have_content("Showing published posts by default")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it "stays quiet on a widened request that overrode every default" do
|
|
179
|
+
# filter_defaults_apply? says yes, but the request answered every defaulted key itself -
|
|
180
|
+
# no default reached the search, and the notice must not claim otherwise.
|
|
181
|
+
visit "/admin/widened_notice_posts?q%5Bstatus_eq%5D=draft&q%5Btitle_cont%5D=drop"
|
|
182
|
+
|
|
183
|
+
expect(page).to have_content("drop me")
|
|
184
|
+
expect(page).to have_no_content("Showing published posts by default")
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it "greets a widened request the defaults actually reached" do
|
|
188
|
+
visit "/admin/widened_notice_posts?q%5Btitle_cont%5D=keep"
|
|
189
|
+
|
|
190
|
+
expect(query_of(page.current_url)["q"])
|
|
191
|
+
.to eq("title_cont" => "keep", "status_eq" => "published")
|
|
192
|
+
expect(page).to have_content("Showing published posts by default")
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it "does not greet a URL that already says what it shows" do
|
|
196
|
+
# The redirected-to URL opened directly - a pasted link. The recipient asked for exactly
|
|
197
|
+
# what the address says, so there is nothing to explain.
|
|
198
|
+
visit "/admin/notice_posts?q%5Bstatus_eq%5D=published"
|
|
199
|
+
|
|
200
|
+
expect(page).to have_no_content("Showing published posts by default")
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails_helper"
|
|
4
|
+
|
|
5
|
+
# `require_any_filter` - an index that answers no question until it is asked one: a bare
|
|
6
|
+
# visit renders an empty collection with the notice instead of the whole table (or, in one
|
|
7
|
+
# inventoried case, instead of a silent model-level `return []`).
|
|
8
|
+
RSpec.describe "require_any_filter", 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
|
+
it "renders nothing on a bare visit, and says why" do
|
|
17
|
+
visit "/admin/picky_posts"
|
|
18
|
+
|
|
19
|
+
expect(page).to have_content("You must specify at least one filter")
|
|
20
|
+
expect(page).to have_no_content("keep me")
|
|
21
|
+
expect(page).to have_no_content("drop me")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "renders an empty result, not Active Admin's blank slate" do
|
|
25
|
+
# The guard blanks the collection; with no `q` in the request Active Admin reads that as
|
|
26
|
+
# "this table has no rows at all" and offers a Create link beside the warning.
|
|
27
|
+
visit "/admin/picky_posts"
|
|
28
|
+
|
|
29
|
+
expect(page).to have_content("No Picky Posts found")
|
|
30
|
+
expect(page).to have_no_content("There are no Picky Posts yet")
|
|
31
|
+
expect(page).to have_no_link("Create one")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "answers once asked" do
|
|
35
|
+
visit "/admin/picky_posts?q%5Btitle_cont%5D=keep"
|
|
36
|
+
|
|
37
|
+
expect(page).to have_no_content("You must specify at least one filter")
|
|
38
|
+
expect(page).to have_content("keep me")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "treats a blank filters-form submission as still unasked" do
|
|
42
|
+
visit "/admin/picky_posts?commit=Filter"
|
|
43
|
+
|
|
44
|
+
expect(page).to have_content("You must specify at least one filter")
|
|
45
|
+
expect(page).to have_no_content("keep me")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "treats an array of blanks as unasked" do
|
|
49
|
+
visit "/admin/picky_posts?q%5Bstatus_eq%5D%5B%5D="
|
|
50
|
+
|
|
51
|
+
expect(page).to have_content("You must specify at least one filter")
|
|
52
|
+
expect(page).to have_no_content("keep me")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "treats a blank-valued filter as unasked" do
|
|
56
|
+
visit "/admin/picky_posts?q%5Btitle_cont%5D="
|
|
57
|
+
|
|
58
|
+
expect(page).to have_content("You must specify at least one filter")
|
|
59
|
+
expect(page).to have_no_content("keep me")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "treats a key no declared filter submits as unasked" do
|
|
63
|
+
# Ransack silently ignores an unknown condition - junk, a stale bookmark after a filter
|
|
64
|
+
# rename - so counting it as a question renders the unfiltered dump behind a valid-looking
|
|
65
|
+
# URL.
|
|
66
|
+
visit "/admin/picky_posts?q%5Bjunk%5D=1"
|
|
67
|
+
|
|
68
|
+
expect(page).to have_content("You must specify at least one filter")
|
|
69
|
+
expect(page).to have_no_content("keep me")
|
|
70
|
+
expect(page).to have_no_content("drop me")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "treats a predicate-less stem of a declared filter as unasked" do
|
|
74
|
+
# q[title] stems to the declared title_cont, but Ransack ignores a key with no
|
|
75
|
+
# predicate - counting it renders the unfiltered dump behind a plausible URL.
|
|
76
|
+
visit "/admin/picky_posts?q%5Btitle%5D=keep"
|
|
77
|
+
|
|
78
|
+
expect(page).to have_content("You must specify at least one filter")
|
|
79
|
+
expect(page).to have_no_content("keep me")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "counts a scope tab as asked" do
|
|
83
|
+
# Active Admin puts a count on the tab, so rendering it empty beside "specify at least one
|
|
84
|
+
# filter" contradicted the tab the admin had just clicked.
|
|
85
|
+
visit "/admin/picky_posts?scope=published"
|
|
86
|
+
|
|
87
|
+
expect(page).to have_no_content("You must specify at least one filter")
|
|
88
|
+
expect(page).to have_content("keep me")
|
|
89
|
+
expect(page).to have_no_content("drop me")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "treats a scope no resource declares as unasked" do
|
|
93
|
+
# Active Admin resolves an unknown scope id to nothing and applies no scope, so counting
|
|
94
|
+
# its mere presence hands back the whole table behind a scope-shaped URL.
|
|
95
|
+
visit "/admin/picky_posts?scope=does_not_exist"
|
|
96
|
+
|
|
97
|
+
expect(page).to have_content("You must specify at least one filter")
|
|
98
|
+
expect(page).to have_no_content("keep me")
|
|
99
|
+
expect(page).to have_no_content("drop me")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "treats a scope no resource declares as unasked on CSV too" do
|
|
103
|
+
# The format that gets no canonicalizing hop - and no warning to read either.
|
|
104
|
+
visit "/admin/picky_posts.csv?scope=does_not_exist"
|
|
105
|
+
|
|
106
|
+
expect(page.body).not_to include("keep me")
|
|
107
|
+
expect(page.body).not_to include("drop me")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "counts a sibling predicate of a declared filter as asked" do
|
|
111
|
+
visit "/admin/picky_posts?q%5Btitle_eq%5D=keep%20me"
|
|
112
|
+
|
|
113
|
+
expect(page).to have_no_content("You must specify at least one filter")
|
|
114
|
+
expect(page).to have_content("keep me")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it "falls back to the shipped text for a notice key no locale defines" do
|
|
118
|
+
visit "/admin/banana_notice_key_posts"
|
|
119
|
+
|
|
120
|
+
expect(page).to have_content("You must specify at least one filter")
|
|
121
|
+
expect(page).to have_no_content("translation missing")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "notices when a policy just reset the only question" do
|
|
125
|
+
# The validator empties q; without re-checking afterwards the whole table renders with no
|
|
126
|
+
# warning - the unfiltered dump the guard was declared to forbid.
|
|
127
|
+
visit "/admin/picky_posts?q%5Btitle_cont%5D=zz9"
|
|
128
|
+
|
|
129
|
+
expect(page).to have_content("You must specify at least one filter")
|
|
130
|
+
expect(page).to have_no_content("keep me")
|
|
131
|
+
end
|
|
132
|
+
end
|