ransack 4.3.0 → 4.4.1

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 (117) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -4
  3. data/lib/polyamorous/polyamorous.rb +1 -1
  4. data/lib/ransack/adapters/active_record/context.rb +30 -3
  5. data/lib/ransack/context.rb +3 -0
  6. data/lib/ransack/helpers/form_builder.rb +6 -7
  7. data/lib/ransack/helpers/form_helper.rb +86 -20
  8. data/lib/ransack/locale/ja.yml +51 -51
  9. data/lib/ransack/locale/ko.yml +6 -6
  10. data/lib/ransack/locale/uk.yml +72 -0
  11. data/lib/ransack/nodes/condition.rb +35 -5
  12. data/lib/ransack/nodes/grouping.rb +1 -1
  13. data/lib/ransack/nodes/sort.rb +1 -1
  14. data/lib/ransack/nodes/value.rb +1 -1
  15. data/lib/ransack/search.rb +1 -1
  16. data/lib/ransack/version.rb +1 -1
  17. data/lib/ransack.rb +4 -0
  18. data/spec/console.rb +3 -15
  19. data/spec/factories/articles.rb +7 -0
  20. data/spec/factories/comments.rb +7 -0
  21. data/spec/factories/notes.rb +13 -0
  22. data/spec/factories/people.rb +10 -0
  23. data/spec/factories/tags.rb +5 -0
  24. data/spec/polyamorous/join_association_spec.rb +0 -1
  25. data/spec/polyamorous/join_dependency_spec.rb +0 -1
  26. data/spec/ransack/adapters/active_record/base_spec.rb +139 -2
  27. data/spec/ransack/adapters/active_record/context_spec.rb +72 -0
  28. data/spec/ransack/helpers/form_builder_spec.rb +0 -2
  29. data/spec/ransack/helpers/form_helper_spec.rb +219 -5
  30. data/spec/ransack/invalid_search_error_spec.rb +27 -0
  31. data/spec/ransack/nodes/condition_spec.rb +229 -0
  32. data/spec/ransack/nodes/grouping_spec.rb +2 -2
  33. data/spec/ransack/nodes/value_spec.rb +12 -1
  34. data/spec/ransack/predicate_spec.rb +0 -1
  35. data/spec/ransack/ransacker_spec.rb +69 -0
  36. data/spec/ransack/search_spec.rb +115 -2
  37. data/spec/ransack/translate_spec.rb +0 -1
  38. data/spec/spec_helper.rb +7 -21
  39. data/spec/support/schema.rb +36 -9
  40. metadata +51 -93
  41. data/.github/FUNDING.yml +0 -3
  42. data/.github/SECURITY.md +0 -12
  43. data/.github/workflows/codeql.yml +0 -72
  44. data/.github/workflows/cronjob.yml +0 -141
  45. data/.github/workflows/deploy.yml +0 -35
  46. data/.github/workflows/rubocop.yml +0 -20
  47. data/.github/workflows/test-deploy.yml +0 -29
  48. data/.github/workflows/test.yml +0 -183
  49. data/.gitignore +0 -7
  50. data/.nojekyll +0 -0
  51. data/.rubocop.yml +0 -50
  52. data/CHANGELOG.md +0 -1193
  53. data/CONTRIBUTING.md +0 -171
  54. data/Gemfile +0 -58
  55. data/Rakefile +0 -24
  56. data/bug_report_templates/test-ransack-scope-and-column-same-name.rb +0 -78
  57. data/bug_report_templates/test-ransacker-arel-present-predicate.rb +0 -75
  58. data/docs/.gitignore +0 -19
  59. data/docs/.nojekyll +0 -0
  60. data/docs/babel.config.js +0 -3
  61. data/docs/blog/2022-03-27-ransack-3.0.0.md +0 -20
  62. data/docs/docs/getting-started/_category_.json +0 -4
  63. data/docs/docs/getting-started/advanced-mode.md +0 -46
  64. data/docs/docs/getting-started/configuration.md +0 -47
  65. data/docs/docs/getting-started/search-matches.md +0 -67
  66. data/docs/docs/getting-started/simple-mode.md +0 -289
  67. data/docs/docs/getting-started/sorting.md +0 -71
  68. data/docs/docs/getting-started/using-predicates.md +0 -282
  69. data/docs/docs/going-further/_category_.json +0 -4
  70. data/docs/docs/going-further/acts-as-taggable-on.md +0 -114
  71. data/docs/docs/going-further/associations.md +0 -70
  72. data/docs/docs/going-further/custom-predicates.md +0 -52
  73. data/docs/docs/going-further/documentation.md +0 -43
  74. data/docs/docs/going-further/exporting-to-csv.md +0 -49
  75. data/docs/docs/going-further/external-guides.md +0 -57
  76. data/docs/docs/going-further/form-customisation.md +0 -63
  77. data/docs/docs/going-further/i18n.md +0 -53
  78. data/docs/docs/going-further/img/create_release.png +0 -0
  79. data/docs/docs/going-further/merging-searches.md +0 -41
  80. data/docs/docs/going-further/other-notes.md +0 -425
  81. data/docs/docs/going-further/polymorphic-search.md +0 -46
  82. data/docs/docs/going-further/ransackers.md +0 -331
  83. data/docs/docs/going-further/release_process.md +0 -36
  84. data/docs/docs/going-further/saving-queries.md +0 -82
  85. data/docs/docs/going-further/searching-postgres.md +0 -57
  86. data/docs/docs/going-further/wiki-contributors.md +0 -82
  87. data/docs/docs/intro.md +0 -99
  88. data/docs/docusaurus.config.js +0 -120
  89. data/docs/package.json +0 -42
  90. data/docs/sidebars.js +0 -31
  91. data/docs/src/components/HomepageFeatures/index.js +0 -64
  92. data/docs/src/components/HomepageFeatures/styles.module.css +0 -11
  93. data/docs/src/css/custom.css +0 -39
  94. data/docs/src/pages/index.module.css +0 -23
  95. data/docs/src/pages/markdown-page.md +0 -7
  96. data/docs/static/.nojekyll +0 -0
  97. data/docs/static/img/docusaurus.png +0 -0
  98. data/docs/static/img/favicon.ico +0 -0
  99. data/docs/static/img/logo.svg +0 -1
  100. data/docs/static/img/tutorial/docsVersionDropdown.png +0 -0
  101. data/docs/static/img/tutorial/localeDropdown.png +0 -0
  102. data/docs/static/img/undraw_docusaurus_mountain.svg +0 -171
  103. data/docs/static/img/undraw_docusaurus_react.svg +0 -170
  104. data/docs/static/img/undraw_docusaurus_tree.svg +0 -40
  105. data/docs/static/logo/ransack-h.png +0 -0
  106. data/docs/static/logo/ransack-h.svg +0 -34
  107. data/docs/static/logo/ransack-v.png +0 -0
  108. data/docs/static/logo/ransack-v.svg +0 -34
  109. data/docs/static/logo/ransack.png +0 -0
  110. data/docs/static/logo/ransack.svg +0 -21
  111. data/docs/yarn.lock +0 -8884
  112. data/ransack.gemspec +0 -26
  113. data/spec/blueprints/articles.rb +0 -5
  114. data/spec/blueprints/comments.rb +0 -5
  115. data/spec/blueprints/notes.rb +0 -5
  116. data/spec/blueprints/people.rb +0 -8
  117. data/spec/blueprints/tags.rb +0 -3
@@ -0,0 +1,10 @@
1
+ FactoryBot.define do
2
+ factory :person do
3
+ name { Faker::Name.name }
4
+ email { "test@example.com" }
5
+ sequence(:salary) { |n| 30000 + (n * 1000) }
6
+ only_sort { Faker::Lorem.words(number: 3).join(' ') }
7
+ only_search { Faker::Lorem.words(number: 3).join(' ') }
8
+ only_admin { Faker::Lorem.words(number: 3).join(' ') }
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ FactoryBot.define do
2
+ factory :tag do
3
+ name { Faker::Lorem.words(number: 3).join(' ') }
4
+ end
5
+ end
@@ -2,7 +2,6 @@ require 'spec_helper'
2
2
 
3
3
  module Polyamorous
4
4
  describe JoinAssociation do
5
-
6
5
  let(:join_dependency) { new_join_dependency Note, {} }
7
6
  let(:reflection) { Note.reflect_on_association(:notable) }
8
7
  let(:parent) { join_dependency.send(:join_root) }
@@ -2,7 +2,6 @@ require 'spec_helper'
2
2
 
3
3
  module Polyamorous
4
4
  describe JoinDependency do
5
-
6
5
  context 'with symbol joins' do
7
6
  subject { new_join_dependency Person, articles: :comments }
8
7
 
@@ -4,7 +4,6 @@ module Ransack
4
4
  module Adapters
5
5
  module ActiveRecord
6
6
  describe Base do
7
-
8
7
  subject { ::ActiveRecord::Base }
9
8
 
10
9
  it { should respond_to :ransack }
@@ -124,7 +123,6 @@ module Ransack
124
123
  expect(s.result.to_sql).to (include rails7_and_mysql ? %q{age > '0'} : 'age > 0')
125
124
  end
126
125
  end
127
-
128
126
  end
129
127
 
130
128
  it 'does not raise exception for string :params argument' do
@@ -195,6 +193,41 @@ module Ransack
195
193
  end
196
194
  end
197
195
 
196
+ context 'negative conditions on related object with HABTM associations' do
197
+ let(:medieval) { Tag.create!(name: 'Medieval') }
198
+ let(:fantasy) { Tag.create!(name: 'Fantasy') }
199
+ let(:arthur) { Article.create!(title: 'King Arthur') }
200
+ let(:marco) { Article.create!(title: 'Marco Polo') }
201
+ let(:comment_arthur) { marco.comments.create!(body: 'King Arthur comment') }
202
+ let(:comment_marco) { arthur.comments.create!(body: 'Marco Polo comment') }
203
+
204
+ before do
205
+ comment_arthur.tags << medieval
206
+ comment_marco.tags << fantasy
207
+ end
208
+
209
+ it 'removes redundant joins from top query' do
210
+ s = Article.ransack(comments_tags_name_not_eq: "Fantasy")
211
+ sql = s.result.to_sql
212
+ expect(sql).to include('LEFT OUTER JOIN')
213
+ end
214
+
215
+ it 'handles != for single values' do
216
+ s = Article.ransack(comments_tags_name_not_eq: "Fantasy")
217
+ articles = s.result.to_a
218
+ expect(articles).to include marco
219
+ expect(articles).to_not include arthur
220
+ end
221
+
222
+ it 'handles NOT IN for multiple attributes' do
223
+ s = Article.ransack(comments_tags_name_not_in: ["Fantasy", "Scifi"])
224
+ articles = s.result.to_a
225
+
226
+ expect(articles).to include marco
227
+ expect(articles).to_not include arthur
228
+ end
229
+ end
230
+
198
231
  context 'negative conditions on self-referenced associations' do
199
232
  let(:pop) { Person.create!(name: 'Grandpa') }
200
233
  let(:dad) { Person.create!(name: 'Father') }
@@ -378,6 +411,63 @@ module Ransack
378
411
  expect(s.result.to_a).to eq [p]
379
412
  end
380
413
 
414
+ if ::ActiveRecord::VERSION::MAJOR >= 7 && ActiveRecord::Base.respond_to?(:normalizes)
415
+ context 'with ActiveRecord::normalizes' do
416
+ around(:each) do |example|
417
+ # Create a temporary model class with normalization for testing
418
+ test_class = Class.new(ActiveRecord::Base) do
419
+ self.table_name = 'people'
420
+ normalizes :name, with: ->(name) { name.gsub(/[^a-z0-9]/, '_') }
421
+
422
+ def self.ransackable_attributes(auth_object = nil)
423
+ Person.ransackable_attributes(auth_object)
424
+ end
425
+
426
+ def self.name
427
+ 'TestPersonWithNormalization'
428
+ end
429
+ end
430
+
431
+ stub_const('TestPersonWithNormalization', test_class)
432
+ example.run
433
+ end
434
+
435
+ it 'should not apply normalization to LIKE wildcards for cont predicate' do
436
+ # Create a person with characters that would be normalized
437
+ p = TestPersonWithNormalization.create!(name: 'foo%bar')
438
+ expect(p.reload.name).to eq('foo_bar') # Verify normalization happened on storage
439
+
440
+ # Search should find the person using the original search term
441
+ s = TestPersonWithNormalization.ransack(name_cont: 'foo')
442
+ expect(s.result.to_a).to eq [p]
443
+
444
+ # Verify the SQL contains proper LIKE wildcards, not normalized ones
445
+ sql = s.result.to_sql
446
+ expect(sql).to include("LIKE '%foo%'")
447
+ expect(sql).not_to include("LIKE '_foo_'")
448
+ end
449
+
450
+ it 'should not apply normalization to LIKE wildcards for other LIKE predicates' do
451
+ p = TestPersonWithNormalization.create!(name: 'foo%bar')
452
+
453
+ # Test start predicate
454
+ s = TestPersonWithNormalization.ransack(name_start: 'foo')
455
+ expect(s.result.to_a).to eq [p]
456
+ expect(s.result.to_sql).to include("LIKE 'foo%'")
457
+
458
+ # Test end predicate
459
+ s = TestPersonWithNormalization.ransack(name_end: 'bar')
460
+ expect(s.result.to_a).to eq [p]
461
+ expect(s.result.to_sql).to include("LIKE '%bar'")
462
+
463
+ # Test i_cont predicate
464
+ s = TestPersonWithNormalization.ransack(name_i_cont: 'FOO')
465
+ expect(s.result.to_a).to eq [p]
466
+ expect(s.result.to_sql).to include("LIKE '%foo%'")
467
+ end
468
+ end
469
+ end
470
+
381
471
  context 'searching by underscores' do
382
472
  # when escaping is supported right in LIKE expression without adding extra expressions
383
473
  def self.simple_escaping?
@@ -412,6 +502,15 @@ module Ransack
412
502
  expect(s.result.map(&:id)).to eq [3, 2, 1]
413
503
  end
414
504
 
505
+ it 'should function correctly with HABTM associations' do
506
+ article = Article.first
507
+ tag = article.tags.first
508
+ s = Person.ransack(article_tags_in: [tag.id])
509
+
510
+ expect(s.result.count).to be 1
511
+ expect(s.result.map(&:id)).to eq [article.person.id]
512
+ end
513
+
415
514
  it 'should function correctly when passing an array of strings' do
416
515
  a, b = Person.select(:id).order(:id).limit(2).map { |a| a.id.to_s }
417
516
 
@@ -630,6 +729,44 @@ module Ransack
630
729
  end
631
730
  end
632
731
 
732
+ context 'ransacker with different types' do
733
+ it 'handles string type ransacker correctly' do
734
+ s = Person.ransack(name_case_insensitive_eq: 'test')
735
+ expect(s.result.to_sql).to match(/LOWER\(.*\) = 'test'/)
736
+ end
737
+
738
+ it 'handles integer type ransacker correctly' do
739
+ s = Person.ransack(sql_literal_id_eq: 1)
740
+ expect(s.result.to_sql).to match(/people\.id = 1/)
741
+ end
742
+ end
743
+
744
+ context 'ransacker with formatter returning nil' do
745
+ it 'handles formatter returning nil gracefully' do
746
+ # This tests the edge case where a formatter might return nil
747
+ s = Person.ransack(article_tags_eq: 999999) # Non-existent tag ID
748
+ expect { s.result.to_sql }.not_to raise_error
749
+ end
750
+ end
751
+
752
+ context 'ransacker with array formatters' do
753
+ it 'handles array_people_ids formatter correctly' do
754
+ person1 = Person.create!(name: 'Test1')
755
+ person2 = Person.create!(name: 'Test2')
756
+
757
+ s = Person.ransack(array_people_ids_eq: 'test')
758
+ expect { s.result }.not_to raise_error
759
+ end
760
+
761
+ it 'handles array_where_people_ids formatter correctly' do
762
+ person1 = Person.create!(name: 'Test1')
763
+ person2 = Person.create!(name: 'Test2')
764
+
765
+ s = Person.ransack(array_where_people_ids_eq: [person1.id, person2.id])
766
+ expect { s.result }.not_to raise_error
767
+ end
768
+ end
769
+
633
770
  context 'regular sorting' do
634
771
  it 'allows sort by desc' do
635
772
  search = Person.ransack(sorts: ['name desc'])
@@ -97,6 +97,62 @@ module Ransack
97
97
 
98
98
  expect(search.result.to_sql).to match /.comments.\..person_id. = .people.\..id./
99
99
  end
100
+
101
+ it 'handles Arel::Nodes::And with children' do
102
+ # Create a mock Arel::Nodes::And with children for testing
103
+ search = Search.new(Person, { articles_title_not_eq: 'some_title', articles_body_not_eq: 'some_body' }, context: subject)
104
+ attribute = search.conditions.first.attributes.first
105
+ constraints = subject.build_correlated_subquery(attribute.parent).constraints
106
+ constraint = constraints.first
107
+
108
+ expect(constraints.length).to eql 1
109
+ expect(constraint.left.name).to eql 'person_id'
110
+ expect(constraint.left.relation.name).to eql 'articles'
111
+ expect(constraint.right.name).to eql 'id'
112
+ expect(constraint.right.relation.name).to eql 'people'
113
+ end
114
+
115
+ it 'correctly extracts correlated key from complex AND conditions' do
116
+ # Test with multiple nested conditions to ensure the children traversal works
117
+ search = Search.new(
118
+ Person,
119
+ {
120
+ articles_title_not_eq: 'title',
121
+ articles_body_not_eq: 'body',
122
+ articles_published_eq: true
123
+ },
124
+ context: subject
125
+ )
126
+
127
+ attribute = search.conditions.first.attributes.first
128
+ constraints = subject.build_correlated_subquery(attribute.parent).constraints
129
+ constraint = constraints.first
130
+
131
+ expect(constraints.length).to eql 1
132
+ expect(constraint.left.relation.name).to eql 'articles'
133
+ expect(constraint.left.name).to eql 'person_id'
134
+ expect(constraint.right.relation.name).to eql 'people'
135
+ expect(constraint.right.name).to eql 'id'
136
+ end
137
+
138
+ it 'build correlated subquery for polymorphic & default_scope when predicate is not_cont_all' do
139
+ search = Search.new(Article,
140
+ g: [
141
+ {
142
+ m: "and",
143
+ c: [
144
+ {
145
+ a: ["recent_notes_note"],
146
+ p: "not_eq",
147
+ v: ["some_note"],
148
+ }
149
+ ]
150
+ }
151
+ ],
152
+ )
153
+
154
+ expect(search.result.to_sql).to match /(.notes.\..note. != \'some_note\')/
155
+ end
100
156
  end
101
157
 
102
158
  describe 'sharing context across searches' do
@@ -141,6 +197,22 @@ module Ransack
141
197
  expect(attribute.relation.table_alias).to be_nil
142
198
  end
143
199
 
200
+ describe '#type_for' do
201
+ it 'returns nil when column does not exist instead of raising NoMethodError' do
202
+ # Create a mock attribute that references a non-existent column
203
+ mock_attr = double('attribute')
204
+ allow(mock_attr).to receive(:valid?).and_return(true)
205
+
206
+ mock_arel_attr = double('arel_attribute')
207
+ allow(mock_arel_attr).to receive(:relation).and_return(Person.arel_table)
208
+ allow(mock_arel_attr).to receive(:name).and_return('nonexistent_column')
209
+ allow(mock_attr).to receive(:arel_attribute).and_return(mock_arel_attr)
210
+ allow(mock_attr).to receive(:klass).and_return(Person)
211
+
212
+ # This should return nil instead of raising an error
213
+ expect(subject.type_for(mock_attr)).to be_nil
214
+ end
215
+ end
144
216
  end
145
217
  end
146
218
  end
@@ -3,7 +3,6 @@ require 'spec_helper'
3
3
  module Ransack
4
4
  module Helpers
5
5
  describe FormBuilder do
6
-
7
6
  router = ActionDispatch::Routing::RouteSet.new
8
7
  router.draw do
9
8
  resources :people, :comments, :notes
@@ -165,7 +164,6 @@ module Ransack
165
164
  def date_select_html(val)
166
165
  %(<option value="#{val}" selected="selected">#{val}</option>)
167
166
  end
168
-
169
167
  end
170
168
  end
171
169
  end
@@ -3,7 +3,6 @@ require 'spec_helper'
3
3
  module Ransack
4
4
  module Helpers
5
5
  describe FormHelper do
6
-
7
6
  router = ActionDispatch::Routing::RouteSet.new
8
7
  router.draw do
9
8
  resources :people, :notes
@@ -458,9 +457,7 @@ module Ransack
458
457
  end
459
458
 
460
459
  context 'view has existing parameters' do
461
-
462
460
  describe '#sort_link should not remove existing params' do
463
-
464
461
  before { @controller.view_context.params[:exist] = 'existing' }
465
462
 
466
463
  subject {
@@ -478,7 +475,6 @@ module Ransack
478
475
  end
479
476
 
480
477
  describe '#sort_url should not remove existing params' do
481
-
482
478
  before { @controller.view_context.params[:exist] = 'existing' }
483
479
 
484
480
  subject {
@@ -496,12 +492,12 @@ module Ransack
496
492
  end
497
493
 
498
494
  context 'using a real ActionController::Parameter object' do
499
-
500
495
  describe 'with symbol q:, #sort_link should include search params' do
501
496
  subject { @controller.view_context.sort_link(Person.ransack, :name) }
502
497
  let(:params) { ActionController::Parameters.new(
503
498
  { q: { name_eq: 'TEST' }, controller: 'people' }
504
499
  ) }
500
+
505
501
  before { @controller.instance_variable_set(:@params, params) }
506
502
 
507
503
  it {
@@ -517,6 +513,7 @@ module Ransack
517
513
  let(:params) { ActionController::Parameters.new(
518
514
  { q: { name_eq: 'TEST' }, controller: 'people' }
519
515
  ) }
516
+
520
517
  before { @controller.instance_variable_set(:@params, params) }
521
518
 
522
519
  it {
@@ -533,6 +530,7 @@ module Ransack
533
530
  ActionController::Parameters.new(
534
531
  { 'q' => { name_eq: 'Test2' }, controller: 'people' }
535
532
  ) }
533
+
536
534
  before { @controller.instance_variable_set(:@params, params) }
537
535
 
538
536
  it {
@@ -549,6 +547,7 @@ module Ransack
549
547
  ActionController::Parameters.new(
550
548
  { 'q' => { name_eq: 'Test2' }, controller: 'people' }
551
549
  ) }
550
+
552
551
  before { @controller.instance_variable_set(:@params, params) }
553
552
 
554
553
  it {
@@ -850,12 +849,227 @@ module Ransack
850
849
  before do
851
850
  Ransack.configure { |c| c.search_key = :example }
852
851
  end
852
+ after do
853
+ Ransack.configure { |c| c.search_key = :q }
854
+ end
853
855
  subject {
854
856
  @controller.view_context
855
857
  .search_form_for(Person.ransack) { |f| f.text_field :name_eq }
856
858
  }
857
859
  it { should match /example_name_eq/ }
858
860
  end
861
+
862
+ describe '#search_form_with with default format' do
863
+ subject { @controller.view_context
864
+ .search_form_with(model: Person.ransack) {} }
865
+ it { should match /action="\/people"/ }
866
+ end
867
+
868
+ describe '#search_form_with with pdf format' do
869
+ subject {
870
+ @controller.view_context
871
+ .search_form_with(model: Person.ransack, format: :pdf) {}
872
+ }
873
+ it { should match /action="\/people.pdf"/ }
874
+ end
875
+
876
+ describe '#search_form_with with json format' do
877
+ subject {
878
+ @controller.view_context
879
+ .search_form_with(model: Person.ransack, format: :json) {}
880
+ }
881
+ it { should match /action="\/people.json"/ }
882
+ end
883
+
884
+ describe '#search_form_with with an array of routes' do
885
+ subject {
886
+ @controller.view_context
887
+ .search_form_with(model: [:admin, Comment.ransack]) {}
888
+ }
889
+ it { should match /action="\/admin\/comments"/ }
890
+ end
891
+
892
+ describe '#search_form_with with custom default search key' do
893
+ before do
894
+ Ransack.configure { |c| c.search_key = :example }
895
+ end
896
+ after do
897
+ Ransack.configure { |c| c.search_key = :q }
898
+ end
899
+ subject {
900
+ @controller.view_context
901
+ .search_form_with(model: Person.ransack) { |f| f.text_field :name_eq }
902
+ }
903
+ it { should match /example\[name_eq\]/ }
904
+ end
905
+
906
+ describe '#search_form_with without Ransack::Search object' do
907
+ it 'raises ArgumentError' do
908
+ expect {
909
+ @controller.view_context.search_form_with(model: "not a search object") {}
910
+ }.to raise_error(ArgumentError, 'No Ransack::Search object was provided to search_form_with!')
911
+ end
912
+ end
913
+
914
+ describe '#turbo_search_form_for with default options' do
915
+ subject {
916
+ @controller.view_context
917
+ .turbo_search_form_for(Person.ransack) {}
918
+ }
919
+ it { should match /action="\/people"/ }
920
+ it { should match /method="post"/ }
921
+ it { should match /data-turbo-action="advance"/ }
922
+ end
923
+
924
+ describe '#turbo_search_form_for with custom method' do
925
+ subject {
926
+ @controller.view_context
927
+ .turbo_search_form_for(Person.ransack, method: :patch) {}
928
+ }
929
+ it { should match /method="post"/ }
930
+ it { should match /name="_method" value="patch"/ }
931
+ it { should match /data-turbo-action="advance"/ }
932
+ end
933
+
934
+ describe '#turbo_search_form_for with turbo_frame' do
935
+ subject {
936
+ @controller.view_context
937
+ .turbo_search_form_for(Person.ransack, turbo_frame: 'search_results') {}
938
+ }
939
+ it { should match /data-turbo-frame="search_results"/ }
940
+ end
941
+
942
+ describe '#turbo_search_form_for with custom turbo_action' do
943
+ subject {
944
+ @controller.view_context
945
+ .turbo_search_form_for(Person.ransack, turbo_action: 'replace') {}
946
+ }
947
+ it { should match /data-turbo-action="replace"/ }
948
+ end
949
+
950
+ describe '#turbo_search_form_for with format' do
951
+ subject {
952
+ @controller.view_context
953
+ .turbo_search_form_for(Person.ransack, format: :json) {}
954
+ }
955
+ it { should match /action="\/people.json"/ }
956
+ end
957
+
958
+ describe '#turbo_search_form_for with array of routes' do
959
+ subject {
960
+ @controller.view_context
961
+ .turbo_search_form_for([:admin, Comment.ransack]) {}
962
+ }
963
+ it { should match /action="\/admin\/comments"/ }
964
+ end
965
+
966
+ describe '#turbo_search_form_for with custom search key' do
967
+ before do
968
+ Ransack.configure { |c| c.search_key = :example }
969
+ end
970
+ after do
971
+ Ransack.configure { |c| c.search_key = :q }
972
+ end
973
+ subject {
974
+ @controller.view_context
975
+ .turbo_search_form_for(Person.ransack) { |f| f.text_field :name_eq }
976
+ }
977
+ it { should match /example_name_eq/ }
978
+ end
979
+
980
+ describe '#turbo_search_form_for without Ransack::Search object' do
981
+ it 'raises ArgumentError' do
982
+ expect {
983
+ @controller.view_context.turbo_search_form_for("not a search object") {}
984
+ }.to raise_error(ArgumentError, 'No Ransack::Search object was provided to turbo_search_form_for!')
985
+ end
986
+ end
987
+
988
+ describe 'private helper methods' do
989
+ let(:helper) { @controller.view_context }
990
+ let(:search) { Person.ransack }
991
+
992
+ describe '#build_turbo_options' do
993
+ it 'builds turbo options with frame' do
994
+ options = { turbo_frame: 'results', turbo_action: 'replace' }
995
+ result = helper.send(:build_turbo_options, options)
996
+ expect(result).to eq({
997
+ data: {
998
+ turbo_frame: 'results',
999
+ turbo_action: 'replace'
1000
+ }
1001
+ })
1002
+ expect(options).to be_empty
1003
+ end
1004
+
1005
+ it 'builds turbo options without frame' do
1006
+ options = { turbo_action: 'advance' }
1007
+ result = helper.send(:build_turbo_options, options)
1008
+ expect(result).to eq({ data: { turbo_action: 'advance' } })
1009
+ end
1010
+
1011
+ it 'uses default turbo action' do
1012
+ options = {}
1013
+ result = helper.send(:build_turbo_options, options)
1014
+ expect(result).to eq({ data: { turbo_action: 'advance' } })
1015
+ end
1016
+ end
1017
+
1018
+ describe '#build_html_options' do
1019
+ it 'builds HTML options with correct method' do
1020
+ options = { class: 'custom' }
1021
+ result = helper.send(:build_html_options, search, options, :post)
1022
+ expect(result[:method]).to eq(:post)
1023
+ expect(result[:class]).to include('custom')
1024
+ end
1025
+ end
1026
+
1027
+ describe '#extract_search_and_set_url' do
1028
+ it 'extracts search from Ransack::Search object' do
1029
+ options = {}
1030
+ result = helper.send(:extract_search_and_set_url, search, options, 'search_form_for')
1031
+ expect(result).to eq(search)
1032
+ expect(options[:url]).to match(/people/)
1033
+ end
1034
+
1035
+ it 'extracts search from array with Search object' do
1036
+ options = {}
1037
+ comment_search = Comment.ransack
1038
+ result = helper.send(:extract_search_and_set_url, [:admin, comment_search], options, 'search_form_for')
1039
+ expect(result).to eq(comment_search)
1040
+ expect(options[:url]).to match(/admin/)
1041
+ end
1042
+
1043
+ it 'raises error for invalid record with correct method name' do
1044
+ options = {}
1045
+ expect {
1046
+ helper.send(:extract_search_and_set_url, "invalid", options, 'turbo_search_form_for')
1047
+ }.to raise_error(ArgumentError, 'No Ransack::Search object was provided to turbo_search_form_for!')
1048
+ end
1049
+
1050
+ it 'extracts search from Ransack::Search object for search_form_with' do
1051
+ options = {}
1052
+ result = helper.send(:extract_search_and_set_url, search, options, 'search_form_with')
1053
+ expect(result).to eq(search)
1054
+ expect(options[:url]).to match(/people/)
1055
+ end
1056
+
1057
+ it 'extracts search from array with Search object for search_form_with' do
1058
+ options = {}
1059
+ comment_search = Comment.ransack
1060
+ result = helper.send(:extract_search_and_set_url, [:admin, comment_search], options, 'search_form_with')
1061
+ expect(result).to eq(comment_search)
1062
+ expect(options[:url]).to match(/admin/)
1063
+ end
1064
+
1065
+ it 'raises error for invalid record with correct method name for search_form_with' do
1066
+ options = {}
1067
+ expect {
1068
+ helper.send(:extract_search_and_set_url, "invalid", options, 'search_form_with')
1069
+ }.to raise_error(ArgumentError, 'No Ransack::Search object was provided to search_form_with!')
1070
+ end
1071
+ end
1072
+ end
859
1073
  end
860
1074
  end
861
1075
  end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ module Ransack
4
+ describe InvalidSearchError do
5
+ it 'inherits from ArgumentError' do
6
+ expect(InvalidSearchError.superclass).to eq(ArgumentError)
7
+ end
8
+
9
+ it 'can be instantiated with a message' do
10
+ error = InvalidSearchError.new('Test error message')
11
+ expect(error.message).to eq('Test error message')
12
+ end
13
+
14
+ it 'can be instantiated without a message' do
15
+ error = InvalidSearchError.new
16
+ expect(error.message).to eq('Ransack::InvalidSearchError')
17
+ end
18
+
19
+ it 'can be raised and caught' do
20
+ expect { raise InvalidSearchError.new('Test') }.to raise_error(InvalidSearchError, 'Test')
21
+ end
22
+
23
+ it 'can be raised and caught as ArgumentError' do
24
+ expect { raise InvalidSearchError.new('Test') }.to raise_error(ArgumentError, 'Test')
25
+ end
26
+ end
27
+ end