Back to Repositories

Validating RailsAdmin Interface Components in rails_admin

This integration test suite validates core functionality of the RailsAdmin gem, covering authentication, localization, UI components, and user interactions. The tests ensure proper rendering, asset loading, and behavior of key administrative interface elements.

Test Coverage Overview

The test suite provides comprehensive coverage of RailsAdmin’s key features including:

  • Authentication and user session handling
  • Localization and internationalization support
  • Asset loading and custom theming
  • Navigation components (navbar, sidebar)
  • CSRF protection verification
  • Turbo Drive integration
  • Error handling for invalid routes

Implementation Analysis

The testing approach utilizes RSpec request specs with Capybara for browser simulation. Tests follow a behavior-driven development pattern, with focused contexts and descriptive specifications. The implementation leverages RSpec’s shared examples and before hooks for setup, while making extensive use of Capybara’s DOM interaction methods.

Technical Details

  • Testing Framework: RSpec with Capybara
  • Authentication: Warden integration
  • JavaScript Testing: JS-enabled specs for dynamic features
  • Asset Handling: Both Sprockets and Webpacker configurations
  • Factory Usage: FactoryBot for test data generation
  • Browser Simulation: Capybara matchers and DOM interactions

Best Practices Demonstrated

The test suite exemplifies several testing best practices including isolation of concerns, thorough edge case coverage, and comprehensive feature validation. Notable practices include:

  • Proper test setup and teardown
  • Consistent use of expectations and matchers
  • Comprehensive error scenario coverage
  • Clear test organization and naming
  • Effective use of RSpec’s DSL features

railsadminteam/rails_admin

spec/integration/rails_admin_spec.rb

            
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe RailsAdmin, type: :request do
  subject { page }

  before do
    RailsAdmin::Config.authenticate_with { warden.authenticate! scope: :user }
    RailsAdmin::Config.current_user_method(&:current_user)
    login_as User.create(
      email: '[email protected]',
      password: 'password',
    )
  end

  # A common mistake for translators is to forget to change the YAML file's
  # root key from en to their own locale (as people tend to use the English
  # file as template for a new translation).
  describe 'localization' do
    it 'defaults to English' do
      RailsAdmin.config.included_models = []
      visit dashboard_path

      is_expected.to have_content('Site Administration')
      is_expected.to have_content('Dashboard')
    end
  end

  describe 'html head' do
    before { visit dashboard_path }

    # NOTE: the [href^="/asset... syntax matches the start of a value. The reason
    # we just do that is to avoid being confused by rails' asset_ids.
    it 'loads stylesheets in header' do
      case RailsAdmin.config.asset_source
      when :sprockets
        is_expected.to have_selector('head link[href^="/assets/rails_admin"][href$=".css"]', visible: false)
      when :webpacker
        is_expected.to have_no_selector('head link[href~="rails_admin"][href$=".css"]', visible: false)
      end
    end

    it 'loads javascript files in body' do
      case RailsAdmin.config.asset_source
      when :sprockets
        is_expected.to have_selector('head script[src^="/assets/rails_admin"][src$=".js"]', visible: false)
      when :webpacker
        is_expected.to have_selector('head script[src^="/packs-test/js/rails_admin"][src$=".js"]', visible: false)
      end
    end
  end

  describe 'custom theming' do
    before { visit dashboard_path }

    if CI_ASSET == :sprockets
      it 'applies the style overridden by assets in the application', js: true do
        expect(find('.navbar-brand small').style('opacity')).to eq({'opacity' => '0.99'})
      end
    end
  end

  describe 'navbar css class' do
    it 'is set by default' do
      expect(RailsAdmin.config.navbar_css_classes).to eq(%w[navbar-dark bg-primary border-bottom])
    end

    it 'can be configured' do
      RailsAdmin.config do |config|
        config.navbar_css_classes = %w[navbar-light border-bottom]
      end
      visit dashboard_path
      is_expected.to have_css('nav.navbar.navbar-light.border-bottom')
    end
  end

  describe 'sidebar navigation', js: true do
    it 'is collapsible' do
      visit dashboard_path
      is_expected.to have_css('.sidebar .nav-link', text: 'Players')
      click_button 'Navigation'
      is_expected.to have_css('.sidebar .btn-toggle.collapsed')
      is_expected.not_to have_css('.sidebar #navigation.show')
      is_expected.not_to have_css('.sidebar .nav-link', text: 'Players')
    end

    it 'persists over a page transition' do
      visit dashboard_path
      click_button 'Navigation'
      is_expected.to have_css('.sidebar .btn-toggle.collapsed')
      is_expected.not_to have_css('.sidebar #navigation.show')
      find('.player_links .show a').trigger('click')
      is_expected.to have_content 'List of Players'
      is_expected.not_to have_css('.sidebar .nav-link', text: 'Players')
    end
  end

  describe '_current_user' do # https://github.com/railsadminteam/rails_admin/issues/549
    it 'is accessible from the list view' do
      RailsAdmin.config Player do
        list do
          field :name do
            visible do
              bindings[:view]._current_user.email == '[email protected]'
            end
          end

          field :team do
            visible do
              bindings[:view]._current_user.email == '[email protected]'
            end
          end
        end
      end

      visit index_path(model_name: 'player')
      is_expected.to have_selector('.header.name_field')
      is_expected.not_to have_selector('.header.team_field')
    end
  end

  describe 'secondary navigation' do
    it 'has Gravatar image' do
      visit dashboard_path
      is_expected.to have_selector('ul.navbar-nav img[src*="gravatar.com"]')
    end

    it "does not show Gravatar when user doesn't have email method" do
      allow_any_instance_of(User).to receive(:respond_to?).and_return(true)
      allow_any_instance_of(User).to receive(:respond_to?).with(:email).and_return(false)
      allow_any_instance_of(User).to receive(:respond_to?).with(:devise_scope).and_return(false)
      visit dashboard_path
      is_expected.not_to have_selector('ul.nav.pull-right li img')
    end

    it 'does not cause error when email is nil' do
      allow_any_instance_of(User).to receive(:email).and_return(nil)
      visit dashboard_path
      is_expected.to have_selector('body.rails_admin')
    end

    it 'shows a log out link' do
      visit dashboard_path
      is_expected.to have_content 'Log out'
    end

    it 'has bg-danger class on log out link' do
      visit dashboard_path
      is_expected.to have_selector('.bg-danger')
    end

    it 'has links for actions which are marked as show_in_navigation' do
      I18n.backend.store_translations(
        :en, admin: {actions: {
          shown_in_navigation: {menu: 'Look this'},
        }}
      )
      RailsAdmin.config do |config|
        config.actions do
          dashboard do
            show_in_navigation false
          end
          root :shown_in_navigation, :dashboard do
            action_name :dashboard
            show_in_navigation true
          end
        end
      end

      visit dashboard_path
      is_expected.not_to have_css '.root_links li', text: 'Dashboard'
      is_expected.to have_css '.root_links li', text: 'Look this'
    end
  end

  describe 'CSRF protection' do
    before do
      allow_any_instance_of(ActionController::Base).to receive(:protect_against_forgery?).and_return(true)
    end

    it 'is enforced' do
      visit new_path(model_name: 'league')
      fill_in 'league[name]', with: 'National league'
      find('input[name="authenticity_token"]', visible: false).set('invalid token')
      expect { click_button 'Save' }.to raise_error ActionController::InvalidAuthenticityToken
    end
  end

  describe 'Turbo Drive', js: true do
    let(:player) { FactoryBot.create :player }

    it 'does not trigger JS errors by going away from and back to RailsAdmin' do
      visit show_path(model_name: 'player', id: player.id)
      click_link 'Show in app'
      click_link 'Back to admin'
      is_expected.to have_content 'Details for Player'
    end

    it 'triggers rails_admin.dom_ready right after a validation error' do
      visit edit_path(model_name: 'player', id: player.id)
      fill_in 'player[name]', with: 'on steroids'
      find_button('Save').trigger 'click'
      is_expected.to have_content 'Player failed to be updated'
      is_expected.to have_css '.filtering-select[data-input-for="player_team_id"]'
    end

    it 'does not prefetch pages' do
      allow_any_instance_of(RailsAdmin::Config::Actions::Index).to receive(:controller).and_raise('index prefetched')
      visit dashboard_path
      find('.sidebar a.nav-link[href$="/player"]').hover
      sleep 0.3 # Turbo waits 100ms before prefetch
    end
  end

  describe 'dom_ready events', js: true do
    it 'trigger properly' do
      visit dashboard_path
      expect(evaluate_script('domReadyTriggered')).to match_array %w[plainjs/dot jquery/dot]
    end
  end

  context 'with invalid model name' do
    it "redirects to dashboard and inform the user the model wasn't found" do
      visit '/admin/whatever'
      expect(page.driver.status_code).to eq(404)
      expect(find('.alert-danger')).to have_content("Model 'Whatever' could not be found")
    end
  end

  context 'with invalid action' do
    it "redirects to balls index and inform the user the id wasn't found" do
      visit '/admin/ball/545-typo'
      expect(page.driver.status_code).to eq(404)
      expect(find('.alert-danger')).to have_content("Ball with id '545-typo' could not be found")
    end
  end
end