Testing FormHelper Sort Link Implementation in Ransack
This test suite validates the FormHelper module’s functionality in the Ransack gem, focusing on sort_link and sort_url helper methods for handling search and sorting operations. The tests ensure proper URL generation, parameter handling, and view helper behavior.
Test Coverage Overview
Implementation Analysis
Technical Details
Best Practices Demonstrated
activerecord-hackery/ransack
spec/ransack/helpers/form_helper_spec.rb
require 'spec_helper'
module Ransack
module Helpers
describe FormHelper do
router = ActionDispatch::Routing::RouteSet.new
router.draw do
resources :people, :notes
namespace :admin do
resources :comments
end
end
include router.url_helpers
# FIXME: figure out a cleaner way to get this behavior
before do
@controller = ActionView::TestCase::TestController.new
@controller.instance_variable_set(:@_routes, router)
@controller.class_eval { include router.url_helpers }
@controller.view_context_class.class_eval { include router.url_helpers }
end
describe '#sort_link with default search_key' do
subject { @controller.view_context
.sort_link(
[:main_app, Person.ransack(sorts: ['name desc'])],
:name,
controller: 'people'
)
}
it { should match /people\?q(%5B|\[)s(%5D|\])=name\+asc/ }
it { should match /sort_link desc/ }
it { should match /Full Name ▼/ }
end
describe '#sort_url with default search_key' do
subject { @controller.view_context
.sort_url(
[:main_app, Person.ransack(sorts: ['name desc'])],
:name,
controller: 'people'
)
}
it { should match /people\?q(%5B|\[)s(%5D|\])=name\+asc/ }
end
describe '#sort_link with default search_key defined as symbol' do
subject { @controller.view_context
.sort_link(
Person.ransack({ sorts: ['name desc'] }, search_key: :people_search),
:name, controller: 'people'
)
}
it { should match /people\?people_search(%5B|\[)s(%5D|\])=name\+asc/ }
end
describe '#sort_url with default search_key defined as symbol' do
subject { @controller.view_context
.sort_url(
Person.ransack({ sorts: ['name desc'] }, search_key: :people_search),
:name, controller: 'people'
)
}
it { should match /people\?people_search(%5B|\[)s(%5D|\])=name\+asc/ }
end
describe '#sort_link desc through association table defined as symbol' do
subject { @controller.view_context
.sort_link(
Person.ransack({ sorts: 'comments_body asc' }),
:comments_body,
controller: 'people'
)
}
it { should match /people\?q(%5B|\[)s(%5D|\])=comments.body\+desc/ }
it { should match /sort_link asc/ }
it { should match /Body ▲/ }
end
describe '#sort_url desc through association table defined as symbol' do
subject { @controller.view_context
.sort_url(
Person.ransack({ sorts: 'comments_body asc' }),
:comments_body,
controller: 'people'
)
}
it { should match /people\?q(%5B|\[)s(%5D|\])=comments.body\+desc/ }
end
describe '#sort_link through association table defined as a string' do
subject { @controller.view_context
.sort_link(
Person.ransack({ sorts: 'comments.body desc' }),
'comments.body',
controller: 'people'
)
}
it { should match /people\?q(%5B|\[)s(%5D|\])=comments.body\+asc/ }
it { should match /sort_link desc/ }
it { should match /Comments.body ▼/ }
end
describe '#sort_url through association table defined as a string' do
subject { @controller.view_context
.sort_url(
Person.ransack({ sorts: 'comments.body desc' }),
'comments.body',
controller: 'people'
)
}
it { should match /people\?q(%5B|\[)s(%5D|\])=comments.body\+asc/ }
end
describe '#sort_link works even if search params are a blank string' do
before { @controller.view_context.params[:q] = '' }
specify {
expect { @controller.view_context
.sort_link(
Person.ransack(@controller.view_context.params[:q]),
:name,
controller: 'people'
)
}.not_to raise_error
}
end
describe '#sort_url works even if search params are a blank string' do
before { @controller.view_context.params[:q] = '' }
specify {
expect { @controller.view_context
.sort_url(
Person.ransack(@controller.view_context.params[:q]),
:name,
controller: 'people'
)
}.not_to raise_error
}
end
describe '#sort_link works even if search params are a string' do
before { @controller.view_context.params[:q] = 'input error' }
specify {
expect { @controller.view_context
.sort_link(
Person.ransack({}),
:name,
controller: 'people'
)
}.not_to raise_error
}
end
describe '#sort_url works even if search params are a string' do
before { @controller.view_context.params[:q] = 'input error' }
specify {
expect { @controller.view_context
.sort_url(
Person.ransack({}),
:name,
controller: 'people'
)
}.not_to raise_error
}
end
describe '#sort_link with search_key defined as a string' do
subject { @controller.view_context
.sort_link(
Person.ransack(
{ sorts: ['name desc'] }, search_key: 'people_search'
),
:name,
controller: 'people'
)
}
it { should match /people\?people_search(%5B|\[)s(%5D|\])=name\+asc/ }
end
describe '#sort_link with default_order defined with a string key' do
subject { @controller.view_context
.sort_link(
[:main_app, Person.ransack()],
:name,
controller: 'people',
default_order: 'desc'
)
}
it { should_not match /default_order/ }
end
describe '#sort_url with default_order defined with a string key' do
subject { @controller.view_context
.sort_url(
[:main_app, Person.ransack()],
:name,
controller: 'people',
default_order: 'desc'
)
}
it { should_not match /default_order/ }
end
describe '#sort_link with multiple search_keys defined as an array' do
subject { @controller.view_context
.sort_link(
[:main_app, Person.ransack(sorts: ['name desc', 'email asc'])],
:name, [:name, 'email DESC'],
controller: 'people'
)
}
it {
should match(/people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+asc&q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+desc/
)
}
it { should match /sort_link desc/ }
it { should match /Full Name ▼/ }
end
describe '#sort_url with multiple search_keys defined as an array' do
subject { @controller.view_context
.sort_url(
[:main_app, Person.ransack(sorts: ['name desc', 'email asc'])],
:name, [:name, 'email DESC'],
controller: 'people'
)
}
it {
should match(/people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+asc&q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+desc/
)
}
end
describe '#sort_link with multiple search_keys does not break on nil values & ignores them' do
subject { @controller.view_context
.sort_link(
[:main_app, Person.ransack(sorts: ['name desc', nil, 'email', nil])],
:name, [nil, :name, nil, 'email DESC', nil],
controller: 'people'
)
}
it {
should match(/people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+asc&q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+desc/
)
}
it { should match /sort_link desc/ }
it { should match /Full Name ▼/ }
end
describe '#sort_url with multiple search_keys does not break on nil values & ignores them' do
subject { @controller.view_context
.sort_url(
[:main_app, Person.ransack(sorts: ['name desc', nil, 'email', nil])],
:name, [nil, :name, nil, 'email DESC', nil],
controller: 'people'
)
}
it {
should match(/people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+asc&q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+desc/
)
}
end
describe '#sort_link with multiple search_keys should allow a label to be specified' do
subject { @controller.view_context
.sort_link(
[:main_app, Person.ransack(sorts: ['name desc', 'email asc'])],
:name, [:name, 'email DESC'],
'Property Name',
controller: 'people'
)
}
it { should match /Property Name ▼/ }
end
describe '#sort_link with multiple search_keys should flip multiple fields specified without a direction' do
subject { @controller.view_context
.sort_link(
[:main_app, Person.ransack(sorts: ['name desc', 'email asc'])],
:name, [:name, :email],
controller: 'people'
)
}
it {
should match(/people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+asc&q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+desc/
)
}
it { should match /sort_link desc/ }
it { should match /Full Name ▼/ }
end
describe '#sort_url with multiple search_keys should flip multiple fields specified without a direction' do
subject { @controller.view_context
.sort_url(
[:main_app, Person.ransack(sorts: ['name desc', 'email asc'])],
:name, [:name, :email],
controller: 'people'
)
}
it {
should match(/people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+asc&q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+desc/
)
}
end
describe '#sort_link with multiple search_keys and default_order specified as a string' do
subject { @controller.view_context
.sort_link(
[:main_app, Person.ransack()],
:name, [:name, :email],
controller: 'people',
default_order: 'desc'
)
}
it {
should match(/people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+desc&q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+desc/
)
}
it { should match /sort_link/ }
it { should match /Full Name/ }
end
describe '#sort_url with multiple search_keys and default_order specified as a string' do
subject { @controller.view_context
.sort_url(
[:main_app, Person.ransack()],
:name, [:name, :email],
controller: 'people',
default_order: 'desc'
)
}
it {
should match(/people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+desc&q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+desc/
)
}
end
describe '#sort_link with multiple search_keys and default_order specified as a symbol' do
subject { @controller.view_context
.sort_link(
[:main_app, Person.ransack()],
:name, [:name, :email],
controller: 'people',
default_order: :desc
)
}
it {
should match(/people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+desc&q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+desc/
)
}
it { should match /sort_link/ }
it { should match /Full Name/ }
end
describe '#sort_url with multiple search_keys and default_order specified as a symbol' do
subject { @controller.view_context
.sort_url(
[:main_app, Person.ransack],
:name, [:name, :email],
controller: 'people',
default_order: :desc
)
}
it {
should match(/people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+desc&q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+desc/
)
}
end
describe '#sort_link with multiple search_keys should allow multiple default_orders to be specified' do
subject { @controller.view_context
.sort_link(
[:main_app, Person.ransack],
:name, [:name, :email],
controller: 'people',
default_order: { name: 'desc', email: 'asc' }
)
}
it {
should match(/people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+desc&q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+asc/
)
}
it { should match /sort_link/ }
it { should match /Full Name/ }
end
describe '#sort_url with multiple search_keys should allow multiple default_orders to be specified' do
subject { @controller.view_context
.sort_url(
[:main_app, Person.ransack],
:name, [:name, :email],
controller: 'people',
default_order: { name: 'desc', email: 'asc' }
)
}
it {
should match(/people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+desc&q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+asc/
)
}
end
describe '#sort_link with multiple search_keys with multiple default_orders should not override a specified order' do
subject { @controller.view_context
.sort_link(
[:main_app, Person.ransack],
:name, [:name, 'email desc'],
controller: 'people',
default_order: { name: 'desc', email: 'asc' }
)
}
it {
should match(/people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+desc&q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+desc/
)
}
it { should match /sort_link/ }
it { should match /Full Name/ }
end
describe '#sort_url with multiple search_keys with multiple default_orders should not override a specified order' do
subject { @controller.view_context
.sort_url(
[:main_app, Person.ransack],
:name, [:name, 'email desc'],
controller: 'people',
default_order: { name: 'desc', email: 'asc' }
)
}
it {
should match(/people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+desc&q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+desc/
)
}
end
describe "#sort_link on polymorphic association should preserve association model name case" do
subject { @controller.view_context
.sort_link(
[:main_app, Note.ransack],
:notable_of_Person_type_name, "Notable",
controller: 'notes'
)
}
it { should match /notes\?q(%5B|\[)s(%5D|\])=notable_of_Person_type_name\+asc/ }
it { should match /sort_link/ }
it { should match /Notable/ }
end
describe "#sort_url on polymorphic association should preserve association model name case" do
subject { @controller.view_context
.sort_link(
[:main_app, Note.ransack],
:notable_of_Person_type_name, "Notable",
controller: 'notes'
)
}
it { should match /notes\?q(%5B|\[)s(%5D|\])=notable_of_Person_type_name\+asc/ }
end
context 'view has existing parameters' do
describe '#sort_link should not remove existing params' do
before { @controller.view_context.params[:exist] = 'existing' }
subject {
@controller.view_context.sort_link(
Person.ransack(
{ sorts: ['name desc'] },
search_key: 'people_search'
),
:name,
controller: 'people'
)
}
it { should match /exist\=existing/ }
end
describe '#sort_url should not remove existing params' do
before { @controller.view_context.params[:exist] = 'existing' }
subject {
@controller.view_context.sort_url(
Person.ransack(
{ sorts: ['name desc'] },
search_key: 'people_search'
),
:name,
controller: 'people'
)
}
it { should match /exist\=existing/ }
end
context 'using a real ActionController::Parameter object' do
describe 'with symbol q:, #sort_link should include search params' do
subject { @controller.view_context.sort_link(Person.ransack, :name) }
let(:params) { ActionController::Parameters.new(
{ q: { name_eq: 'TEST' }, controller: 'people' }
) }
before { @controller.instance_variable_set(:@params, params) }
it {
should match(
/people\?q(%5B|\[)name_eq(%5D|\])=TEST&q(%5B|\[)s(%5D|\])
=name\+asc/x,
)
}
end
describe 'with symbol q:, #sort_url should include search params' do
subject { @controller.view_context.sort_url(Person.ransack, :name) }
let(:params) { ActionController::Parameters.new(
{ q: { name_eq: 'TEST' }, controller: 'people' }
) }
before { @controller.instance_variable_set(:@params, params) }
it {
should match(
/people\?q(%5B|\[)name_eq(%5D|\])=TEST&q(%5B|\[)s(%5D|\])
=name\+asc/x,
)
}
end
describe "with string 'q', #sort_link should include search params" do
subject { @controller.view_context.sort_link(Person.ransack, :name) }
let(:params) {
ActionController::Parameters.new(
{ 'q' => { name_eq: 'Test2' }, controller: 'people' }
) }
before { @controller.instance_variable_set(:@params, params) }
it {
should match(
/people\?q(%5B|\[)name_eq(%5D|\])=Test2&q(%5B|\[)s(%5D|\])
=name\+asc/x,
)
}
end
describe "with string 'q', #sort_url should include search params" do
subject { @controller.view_context.sort_url(Person.ransack, :name) }
let(:params) {
ActionController::Parameters.new(
{ 'q' => { name_eq: 'Test2' }, controller: 'people' }
) }
before { @controller.instance_variable_set(:@params, params) }
it {
should match(
/people\?q(%5B|\[)name_eq(%5D|\])=Test2&q(%5B|\[)s(%5D|\])
=name\+asc/x,
)
}
end
end
end
describe '#sort_link with hide order indicator set to true' do
subject { @controller.view_context
.sort_link(
[:main_app, Person.ransack(sorts: ['name desc'])],
:name,
controller: 'people',
hide_indicator: true
)
}
it { should match /Full Name/ }
it { should_not match /▼|▲/ }
end
describe '#sort_link with hide order indicator set to false' do
subject { @controller.view_context
.sort_link(
[:main_app, Person.ransack(sorts: ['name desc'])],
:name,
controller: 'people',
hide_indicator: false
)
}
it { should match /Full Name ▼/ }
end
describe '#sort_link with config set with custom up_arrow' do
before do
Ransack.configure { |c| c.custom_arrows = { up_arrow: "\u{1F446}" } }
end
after do
Ransack.configure { |c| c.custom_arrows = { up_arrow: "▼" } }
end
subject { @controller.view_context
.sort_link(
[:main_app, Person.ransack(sorts: ['name desc'])],
:name,
controller: 'people',
hide_indicator: false
)
}
it { should match /Full Name \u{1F446}/ }
end
describe '#sort_link with config set with custom down_arrow' do
before do
Ransack.configure { |c| c.custom_arrows = { down_arrow: "\u{1F447}" } }
end
after do
Ransack.configure { |c| c.custom_arrows = { down_arrow: "▲" } }
end
subject { @controller.view_context
.sort_link(
[:main_app, Person.ransack(sorts: ['name asc'])],
:name,
controller: 'people',
hide_indicator: false
)
}
it { should match /Full Name \u{1F447}/ }
end
describe '#sort_link with config set to hide arrows' do
before do
Ransack.configure { |c| c.hide_sort_order_indicators = true }
end
after do
Ransack.configure { |c| c.hide_sort_order_indicators = false }
end
subject { @controller.view_context
.sort_link(
[:main_app, Person.ransack(sorts: ['name desc'])],
:name,
controller: 'people'
)
}
it { should_not match /▼|▲/ }
end
describe '#sort_link with config set to show arrows (default setting)' do
before do
Ransack.configure { |c| c.hide_sort_order_indicators = false }
end
subject { @controller.view_context
.sort_link(
[:main_app, Person.ransack(sorts: ['name desc'])],
:name,
controller: 'people'
)
}
it { should match /Full Name ▼/ }
end
describe '#sort_link with config set to show arrows and a default arrow set' do
before do
Ransack.configure do |c|
c.hide_sort_order_indicators = false
c.custom_arrows = { default_arrow: "defaultarrow" }
end
end
after do
Ransack.configure do |c|
c.custom_arrows = { default_arrow: nil }
end
end
subject { @controller.view_context
.sort_link(
[:main_app, Person.ransack],
:name,
controller: 'people'
)
}
it { should match /Full Name defaultarrow/ }
end
describe '#sort_link w/config to hide arrows + custom arrow, hides all' do
before do
Ransack.configure do |c|
c.hide_sort_order_indicators = true
c.custom_arrows = { down_arrow: 'down', default_arrow: "defaultarrow" }
end
end
after do
Ransack.configure do |c|
c.hide_sort_order_indicators = false
c.custom_arrows = { down_arrow: '▲' }
end
end
subject { @controller.view_context
.sort_link(
[:main_app, Person.ransack(sorts: ['name desc'])],
:name,
controller: 'people'
)
}
it { should_not match /▼|down|defaultarrow/ }
end
describe '#sort_link with config set to show arrows + custom arrow' do
before do
Ransack.configure do |c|
c.hide_sort_order_indicators = false
c.custom_arrows = { up_arrow: 'up-value' }
end
end
after do
Ransack.configure do |c|
c.hide_sort_order_indicators = false
c.custom_arrows = { up_arrow: '▼' }
end
end
subject { @controller.view_context
.sort_link(
[:main_app, Person.ransack(sorts: ['name desc'])],
:name,
controller: 'people'
)
}
it { should match /▲|up-value/ }
end
describe '#sort_link with a block' do
subject { @controller.view_context
.sort_link(
[:main_app, Person.ransack(sorts: ['name desc'])],
:name,
controller: 'people'
) { 'Block label' }
}
it { should match /Block label ▼/ }
end
describe '#sort_link with class option' do
subject { @controller.view_context
.sort_link(
[:main_app, Person.ransack(sorts: ['name desc'])],
:name,
class: 'people', controller: 'people'
)
}
it { should match /class="sort_link desc people"/ }
it { should_not match /people\?class=people/ }
end
describe '#sort_link with class option workaround' do
it "generates a correct link and prints a deprecation" do
expect do
link = @controller.view_context
.sort_link(
[:main_app, Person.ransack(sorts: ['name desc'])],
:name,
'name',
{ controller: 'people' },
class: 'people'
)
expect(link).to match(/class="sort_link desc people"/)
expect(link).not_to match(/people\?class=people/)
end.to output(
/Passing two trailing hashes to `sort_link` is deprecated, merge the trailing hashes into a single one\. \(called at #{Regexp.escape(__FILE__)}:/
).to_stderr
end
end
describe '#sort_link with data option' do
subject { @controller.view_context
.sort_link(
[:main_app, Person.ransack(sorts: ['name desc'])],
:name,
data: { turbo_action: :advance }, controller: 'people'
)
}
it { should match /data-turbo-action="advance"/ }
it { should_not match /people\?data%5Bturbo_action%5D=advance/ }
end
describe "#sort_link with host option" do
subject { @controller.view_context
.sort_link(
[:main_app, Person.ransack(sorts: ['name desc'])],
:name,
host: 'foo', controller: 'people'
)
}
it { should match /href="\/people\?q/ }
it { should_not match /href=".*foo/ }
end
describe "#sort_link ignores host in params" do
before { @controller.view_context.params[:host] = 'other_domain' }
subject { @controller.view_context.sort_link(Person.ransack, :name, controller: 'people') }
it { should match /href="\/people\?q/ }
it { should_not match /href=".*other_domain/ }
end
describe '#search_form_for with default format' do
subject { @controller.view_context
.search_form_for(Person.ransack) {} }
it { should match /action="\/people"/ }
end
describe '#search_form_for with pdf format' do
subject {
@controller.view_context
.search_form_for(Person.ransack, format: :pdf) {}
}
it { should match /action="\/people.pdf"/ }
end
describe '#search_form_for with json format' do
subject {
@controller.view_context
.search_form_for(Person.ransack, format: :json) {}
}
it { should match /action="\/people.json"/ }
end
describe '#search_form_for with an array of routes' do
subject {
@controller.view_context
.search_form_for([:admin, Comment.ransack]) {}
}
it { should match /action="\/admin\/comments"/ }
end
describe '#search_form_for with custom default search key' do
before do
Ransack.configure { |c| c.search_key = :example }
end
subject {
@controller.view_context
.search_form_for(Person.ransack) { |f| f.text_field :name_eq }
}
it { should match /example_name_eq/ }
end
end
end
end