File: result.rb

package info (click to toggle)
ruby-capybara 3.40.0%2Bds-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,368 kB
  • sloc: ruby: 23,988; javascript: 752; makefile: 11
file content (186 lines) | stat: -rw-r--r-- 4,748 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# frozen_string_literal: true

require 'forwardable'

module Capybara
  ##
  # A {Capybara::Result} represents a collection of {Capybara::Node::Element} on the page. It is possible to interact with this
  # collection similar to an Array because it implements Enumerable and offers the following Array methods through delegation:
  #
  # * \[\]
  # * each()
  # * at()
  # * size()
  # * count()
  # * length()
  # * first()
  # * last()
  # * empty?()
  # * values_at()
  # * sample()
  #
  # @see Capybara::Node::Element
  #
  class Result
    include Enumerable
    extend Forwardable

    def initialize(elements, query)
      @elements = elements
      @result_cache = []
      @filter_errors = []
      @results_enum = lazy_select_elements { |node| query.matches_filters?(node, @filter_errors) }
      @query = query
      @allow_reload = false
    end

    def_delegators :full_results, :size, :length, :last, :values_at, :inspect, :sample, :to_ary

    alias index find_index

    def each(&block)
      return enum_for(:each) unless block

      @result_cache.each(&block)
      loop do
        next_result = @results_enum.next
        add_to_cache(next_result)
        yield next_result
      end
      self
    end

    def [](*args)
      idx, length = args
      max_idx = case idx
      when Integer
        if idx.negative?
          nil
        else
          length.nil? ? idx : idx + length - 1
        end
      when Range
        # idx.max is broken with beginless ranges
        # idx.end && idx.max # endless range will have end == nil
        max = idx.end
        max = nil if max&.negative?
        max -= 1 if max && idx.exclude_end?
        max
      end

      if max_idx.nil?
        full_results[*args]
      else
        load_up_to(max_idx + 1)
        @result_cache[*args]
      end
    end
    alias :at :[]

    def empty?
      !any?
    end

    def compare_count
      return 0 unless @query

      count, min, max, between = @query.options.values_at(:count, :minimum, :maximum, :between)

      # Only check filters for as many elements as necessary to determine result
      if count && (count = Integer(count))
        return load_up_to(count + 1) <=> count
      end

      return -1 if min && (min = Integer(min)) && (load_up_to(min) < min)

      return 1 if max && (max = Integer(max)) && (load_up_to(max + 1) > max)

      if between
        min, max = (between.begin && between.min) || 1, between.end
        max -= 1 if max && between.exclude_end?

        size = load_up_to(max ? max + 1 : min)
        return size <=> min unless between.include?(size)
      end

      0
    end

    def matches_count?
      compare_count.zero?
    end

    def failure_message
      message = @query.failure_message
      if count.zero?
        message << ' but there were no matches'
      else
        message << ", found #{count} #{Capybara::Helpers.declension('match', 'matches', count)}: " \
                << full_results.map { |r| r.text.inspect }.join(', ')
      end
      unless rest.empty?
        elements = rest.map { |el| el.text rescue '<<ERROR>>' }.map(&:inspect).join(', ') # rubocop:disable Style/RescueModifier
        message << '. Also found ' << elements << ', which matched the selector but not all filters. '
        message << @filter_errors.join('. ') if (rest.size == 1) && count.zero?
      end
      message
    end

    def negative_failure_message
      failure_message.sub(/(to find)/, 'not \1')
    end

    def unfiltered_size
      @elements.length
    end

    ##
    # @api private
    #
    def allow_reload!
      @allow_reload = true
      self
    end

  private

    def add_to_cache(elem)
      elem.allow_reload!(@result_cache.size) if @allow_reload
      @result_cache << elem
    end

    def load_up_to(num)
      loop do
        break if @result_cache.size >= num

        add_to_cache(@results_enum.next)
      end
      @result_cache.size
    end

    def full_results
      loop { @result_cache << @results_enum.next }
      @result_cache
    end

    def rest
      @rest ||= @elements - full_results
    end

    if RUBY_PLATFORM == 'java'
      # JRuby < 9.2.8.0 has an issue with lazy enumerators which
      # causes a concurrency issue with network requests here
      # https://github.com/jruby/jruby/issues/4212
      # while JRuby >= 9.2.8.0 leaks threads when using lazy enumerators
      # https://github.com/teamcapybara/capybara/issues/2349
      # so disable the use and JRuby users will need to pay a performance penalty
      def lazy_select_elements(&block)
        @elements.select(&block).to_enum # non-lazy evaluation
      end
    else
      def lazy_select_elements(&block)
        @elements.lazy.select(&block)
      end
    end
  end
end