Back to Repositories

Testing Multi-Select Filtering Widget Integration in Rails Admin

This test suite evaluates the multi-select filtering widget functionality in Rails Admin, focusing on dynamic selection, filtering, and state management. It covers both creation and update scenarios while testing remote requests and browser navigation behaviors.

Test Coverage Overview

The test suite provides comprehensive coverage of the multi-select widget functionality including:

  • Initial state validation
  • Dynamic filtering capabilities
  • Selection and deselection operations
  • Bulk actions (Choose all/Clear all)
  • Remote request handling
  • Browser navigation edge cases
  • Dynamic scoping based on related fields

Implementation Analysis

The testing approach utilizes RSpec’s request specs with JavaScript support, implementing factory-based test data generation. It employs page object interactions and script execution to simulate user behaviors, particularly for keyboard events and filtering operations.

The implementation leverages RSpec’s shared contexts and before hooks for setup, while using precise DOM selectors for element interaction.

Technical Details

  • Testing Framework: RSpec
  • Type: Integration tests with JS support
  • Factory Usage: FactoryBot for test data
  • Key Dependencies: Capybara for browser simulation
  • Configuration: Custom RailsAdmin field configurations
  • DOM Interaction: CSS selectors and JavaScript event dispatch

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Isolated test contexts for different scenarios
  • Comprehensive edge case coverage
  • Clear test case organization
  • Efficient test data setup
  • Proper async operation handling
  • Browser state management testing

railsadminteam/rails_admin

spec/integration/widgets/filtering_multi_select_spec.rb

            
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe 'Filtering multi-select widget', type: :request, js: true do
  subject { page }

  let(:team) { FactoryBot.create :team }
  let!(:players) { ['Cory Burns', 'Leonys Martin', 'Matt Garza'].map { |name| FactoryBot.create :player, name: name } }
  before do
    RailsAdmin.config Team do
      field :players
    end
  end

  context 'on create' do
    before { visit new_path(model_name: 'team') }

    it 'is initially unset' do
      expect(all('.ra-multiselect-collection option').map(&:text)).to match_array ['Leonys Martin', 'Cory Burns', 'Matt Garza']
      expect(find('.ra-multiselect-selection').text).to be_empty
    end

    it 'supports filtering' do
      find('input.ra-multiselect-search').set('Alex')
      page.execute_script("document.querySelector('input.ra-multiselect-search').dispatchEvent(new KeyboardEvent('keydown'))")
      is_expected.to have_content 'No objects found'
      find('input.ra-multiselect-search').set('Ma')
      page.execute_script("document.querySelector('input.ra-multiselect-search').dispatchEvent(new KeyboardEvent('keydown'))")
      is_expected.to have_content 'Leonys'
      expect(all('.ra-multiselect-collection option').map(&:text)).to match_array ['Leonys Martin', 'Matt Garza']
    end

    it 'sets ids of the selected items' do
      find('.ra-multiselect-collection option', text: /Cory/).select_option
      within('.ra-multiselect-center') { click_link 'Add new' }
      expect(all('.ra-multiselect-selection option').map(&:text)).to match_array ['Cory Burns']
      expect(all('.ra-multiselect-collection option').map(&:text)).to match_array ['Leonys Martin', 'Matt Garza']
      expect(all('#team_player_ids option', visible: false).select(&:selected?).map(&:value)).to match_array [players[0].id.to_s]
    end
  end

  context 'on update' do
    let!(:player) { FactoryBot.create :player, team: team, name: 'Elvis Andrus' }
    before { visit edit_path(model_name: 'team', id: team.id) }

    it 'additionally selects items' do
      expect(all('#team_player_ids option', visible: false).select(&:selected?).map(&:value)).to match_array [player.id.to_s]
      find('.ra-multiselect-collection option', text: /Cory/).select_option
      within('.ra-multiselect-center') { click_link 'Add new' }
      expect(all('#team_player_ids option', visible: false).select(&:selected?).map(&:value)).to match_array [players[0].id.to_s, player.id.to_s]
    end

    it 'deselects the current selection' do
      find('.ra-multiselect-selection option', text: /Elvis/).select_option
      within('.ra-multiselect-center') { click_link 'Remove' }
      expect(all('.ra-multiselect-selection option').map(&:text)).to be_empty
      expect(all('.ra-multiselect-collection option').map(&:text)).to match_array ['Cory Burns', 'Leonys Martin', 'Matt Garza', 'Elvis Andrus']
      expect(all('#team_player_ids option', visible: false).select(&:selected?).map(&:value)).to be_empty
    end
  end

  describe 'Choose all button' do
    it 'picks all available items' do
      visit edit_path(model_name: 'team', id: team.id)
      click_link 'Choose all'
      expect(all(:css, '#team_player_ids option', visible: false).map(&:value)).to match_array players.map(&:id).map(&:to_s)
    end
  end

  describe 'Clear all button' do
    let!(:player) { FactoryBot.create :player, team: team, name: 'Elvis Andrus' }

    it 'removes all selected items' do
      visit edit_path(model_name: 'team', id: team.id)
      find('.ra-multiselect-collection option', text: /Cory/).select_option
      within('.ra-multiselect-center') { click_link 'Add new' }
      expect(all('.ra-multiselect-selection option').map(&:text)).to match_array ['Cory Burns', 'Elvis Andrus']
      click_link 'Clear all'
      expect(all(:css, '#team_player_ids option', visible: false).select(&:selected?).map(&:value)).to be_empty
    end
  end

  context 'when using remote requests' do
    before do
      RailsAdmin.config Team do
        field(:players) { associated_collection_cache_all false }
      end
      visit edit_path(model_name: 'team', id: team.id)
    end

    it 'supports filtering' do
      find('input.ra-multiselect-search').set('Alex')
      page.execute_script("document.querySelector('input.ra-multiselect-search').dispatchEvent(new KeyboardEvent('keydown'))")
      is_expected.to have_content 'No objects found'
      players[2].update name: 'Adam Rosales'
      find('input.ra-multiselect-search').set('Ma')
      page.execute_script("document.querySelector('input.ra-multiselect-search').dispatchEvent(new KeyboardEvent('keydown'))")
      is_expected.to have_content 'Leonys'
      expect(all('.ra-multiselect-collection option').map(&:text)).to match_array ['Leonys Martin']
    end

    describe 'Choose all button' do
      it 'does not pick the placeholder for selection' do
        click_link 'Choose all'
        expect(page).not_to have_css('#team_player_ids option', visible: false)
        expect(page).not_to have_css('.ra-multiselect-selection option')
      end

      it 'picks all available items' do
        find('input.ra-multiselect-search').set('Ma')
        page.execute_script("document.querySelector('input.ra-multiselect-search').dispatchEvent(new KeyboardEvent('keydown'))")
        expect(page).to have_css('.ra-multiselect-collection option', text: /Matt/)
        click_link 'Choose all'
        expect(all(:css, '#team_player_ids option', visible: false).map(&:value)).to match_array players[1..2].map(&:id).map(&:to_s)
      end
    end
  end

  it 'does not cause duplication when using browser back' do
    visit new_path(model_name: 'team')
    find(%([href$="/admin/team/export"])).click
    is_expected.to have_content 'Export Teams'
    page.go_back
    is_expected.to have_content 'New Team'
    expect(all(:css, 'input.ra-multiselect-search').count).to eq 1
  end

  describe 'dynamic scoping' do
    let!(:team) { FactoryBot.create :team, division: FactoryBot.create(:division) }
    let(:division) { FactoryBot.create(:division) }
    let!(:teams) { ['Los Angeles Dodgers', 'Texas Rangers'].map { |name| FactoryBot.create :team, name: name, division: division } }
    before do
      RailsAdmin.config Team do
        field :name
        field :division
      end
      RailsAdmin.config Fan do
        field :division, :enum do
          enum { Division.pluck(:name, CI_ORM == :active_record ? :custom_id : :id).to_h }
          def value
            nil
          end

          def parse_input(params)
            params.delete :division
          end
        end
        field :teams do
          dynamically_scope_by :division
        end
      end
      visit new_path(model_name: 'fan')
    end

    it 'changes selection candidates based on value of the specified field' do
      expect(all('#fan_team_ids option', visible: false).map(&:value).filter(&:present?)).to be_empty
      select division.name, from: 'Division', visible: false
      find('input.ra-multiselect-search').set('e')
      page.execute_script("document.querySelector('input.ra-multiselect-search').dispatchEvent(new KeyboardEvent('keydown'))")
      is_expected.to have_content 'Dodgers'
      expect(all('.ra-multiselect-collection option').map(&:text)).to match_array ['Los Angeles Dodgers', 'Texas Rangers']
    end
  end
end