Back to Repositories

Testing XPath Builder Attribute Handling in Capybara

This test suite validates the XPathBuilder functionality in Capybara’s selector system, focusing on attribute condition handling and XPath generation for DOM element selection.

Test Coverage Overview

The test suite comprehensively covers XPathBuilder’s attribute condition handling capabilities:

  • Single and multiple selector handling
  • String-based attribute conditions
  • Regular expression pattern matching
  • Special class attribute handling
  • Complex selector combinations

Implementation Analysis

The testing approach utilizes RSpec’s context-based structure to organize related test cases. It implements builder pattern testing with described_class and let blocks for setup.

Tests verify XPath generation for various selector scenarios, including string matching, regex patterns, and CSS class handling with specific attention to DOM selection accuracy.

Technical Details

  • Testing Framework: RSpec
  • Primary Class: Capybara::Selector::XPathBuilder
  • Test Dependencies: spec_helper
  • Key Methods: add_attribute_conditions
  • Testing Patterns: Builder pattern, Context isolation

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Isolated test contexts for different functionality aspects
  • Comprehensive edge case coverage
  • Clear test case descriptions
  • Consistent assertion patterns
  • Proper test setup and teardown

teamcapybara/capybara

spec/xpath_builder_spec.rb

            
# frozen_string_literal: true

require 'spec_helper'

# rubocop:disable RSpec/InstanceVariable
RSpec.describe Capybara::Selector::XPathBuilder do
  let :builder do
    described_class.new(@xpath)
  end

  context 'add_attribute_conditions' do
    it 'adds a single string condition to a single selector' do
      @xpath = './/div'
      selector = builder.add_attribute_conditions(random: 'abc')
      expect(selector).to eq %((.//div)[(./@random = 'abc')])
    end

    it 'adds multiple string conditions to a single selector' do
      @xpath = './/div'
      selector = builder.add_attribute_conditions(random: 'abc', other: 'def')
      expect(selector).to eq %(((.//div)[(./@random = 'abc')])[(./@other = 'def')])
    end

    it 'adds a single string condition to a multiple selector' do
      @xpath = XPath.descendant(:div, :ul)
      selector = builder.add_attribute_conditions(random: 'abc')
      expect(selector.to_s).to eq @xpath[XPath.attr(:random) == 'abc'].to_s
    end

    it 'adds multiple string conditions to a multiple selector' do
      @xpath = XPath.descendant(:div, :ul)
      selector = builder.add_attribute_conditions(random: 'abc', other: 'def')
      expect(selector.to_s).to eq %(.//*[self::div | self::ul][(./@random = 'abc')][(./@other = 'def')])
    end

    it 'adds simple regexp conditions to a single selector' do
      @xpath = XPath.descendant(:div)
      selector = builder.add_attribute_conditions(random: /abc/, other: /def/)
      expect(selector.to_s).to eq %(.//div[./@random[contains(., 'abc')]][./@other[contains(., 'def')]])
    end

    it 'adds wildcard regexp conditions to a single selector' do
      @xpath = './/div'
      selector = builder.add_attribute_conditions(random: /abc.*def/, other: /def.*ghi/)
      expect(selector).to eq %(((.//div)[./@random[(contains(., 'abc') and contains(., 'def'))]])[./@other[(contains(., 'def') and contains(., 'ghi'))]])
    end

    it 'adds alternated regexp conditions to a single selector' do
      @xpath = XPath.descendant(:div)
      selector = builder.add_attribute_conditions(random: /abc|def/, other: /def|ghi/)
      expect(selector.to_s).to eq %(.//div[./@random[(contains(., 'abc') or contains(., 'def'))]][./@other[(contains(., 'def') or contains(., 'ghi'))]])
    end

    it 'adds alternated regexp conditions to a multiple selector' do
      @xpath = XPath.descendant(:div, :ul)
      selector = builder.add_attribute_conditions(other: /def.*ghi|jkl/)
      expect(selector.to_s).to eq %(.//*[self::div | self::ul][./@other[((contains(., 'def') and contains(., 'ghi')) or contains(., 'jkl'))]])
    end

    it "returns original selector when regexp can't be substringed" do
      @xpath = './/div'
      selector = builder.add_attribute_conditions(other: /.+/)
      expect(selector).to eq '(.//div)[./@other]'
    end

    context ':class' do
      it 'handles string' do
        @xpath = './/a'
        selector = builder.add_attribute_conditions(class: 'my_class')
        expect(selector).to eq %((.//a)[contains(concat(' ', normalize-space(./@class), ' '), ' my_class ')])
      end

      it 'handles negated strings' do
        @xpath = XPath.descendant(:a)
        selector = builder.add_attribute_conditions(class: '!my_class')
        expect(selector.to_s).to eq @xpath[!XPath.attr(:class).contains_word('my_class')].to_s
      end

      it 'handles array of strings' do
        @xpath = './/a'
        selector = builder.add_attribute_conditions(class: %w[my_class my_other_class])
        expect(selector).to eq %((.//a)[(contains(concat(' ', normalize-space(./@class), ' '), ' my_class ') and contains(concat(' ', normalize-space(./@class), ' '), ' my_other_class '))])
      end

      it 'handles array of string when negated included' do
        @xpath = XPath.descendant(:a)
        selector = builder.add_attribute_conditions(class: %w[my_class !my_other_class])
        expect(selector.to_s).to eq @xpath[XPath.attr(:class).contains_word('my_class') & !XPath.attr(:class).contains_word('my_other_class')].to_s
      end
    end
  end
end
# rubocop:enable RSpec/InstanceVariable