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,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