vapir-common 1.7.2 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +0 -5
- data/lib/vapir-common.rb +16 -4
- data/lib/vapir-common/browser.rb +189 -144
- data/lib/vapir-common/browsers.rb +21 -9
- data/lib/vapir-common/config.rb +341 -0
- data/lib/vapir-common/container.rb +160 -30
- data/lib/vapir-common/element.rb +65 -555
- data/lib/vapir-common/element_class_and_module.rb +378 -0
- data/lib/vapir-common/element_collection.rb +108 -20
- data/lib/vapir-common/elements/elements.rb +243 -67
- data/lib/vapir-common/external/core_extensions.rb +62 -0
- data/lib/vapir-common/handle_options.rb +1 -1
- data/lib/vapir-common/keycodes.rb +135 -0
- data/lib/vapir-common/options.rb +5 -38
- data/lib/vapir-common/page_container.rb +26 -21
- data/lib/vapir-common/specifier.rb +2 -2
- data/lib/vapir-common/version.rb +5 -0
- data/lib/vapir-common/waiter.rb +44 -90
- data/lib/vapir.rb +7 -0
- metadata +12 -27
- data/lib/vapir-common/testcase.rb +0 -89
- data/lib/vapir-common/win_window.rb +0 -1227
@@ -0,0 +1,378 @@
|
|
1
|
+
require 'vapir-common/container'
|
2
|
+
|
3
|
+
module Vapir
|
4
|
+
# this module is for methods that should go on both common element modules (ie, TextField) as well
|
5
|
+
# as browser-specific element classes (ie, Firefox::TextField).
|
6
|
+
module ElementClassAndModuleMethods
|
7
|
+
# takes an element_object (JsshObject or WIN32OLE), and finds the most specific class
|
8
|
+
# that is < self whose specifiers match it. Returns an instance of that class using the given
|
9
|
+
# element_object.
|
10
|
+
#
|
11
|
+
# second argument, extra, is passed as the 'extra' argument to the Element constructor (see its documentation).
|
12
|
+
#
|
13
|
+
# if you give a different how/what (third and fourth arguments, optional), then those are passed
|
14
|
+
# to the Element constructor.
|
15
|
+
def factory(element_object, extra={}, *howwhat)
|
16
|
+
curr_klass=self
|
17
|
+
# since this gets included in the Element modules, too, check where we are
|
18
|
+
unless self.is_a?(Class) && self < Vapir::Element
|
19
|
+
raise TypeError, "factory was called on #{self} (#{self.class}), which is not a Class that is < Element"
|
20
|
+
end
|
21
|
+
how, what = *case howwhat.length
|
22
|
+
when 0
|
23
|
+
[:element_object, element_object]
|
24
|
+
when 2
|
25
|
+
howwhat
|
26
|
+
else
|
27
|
+
raise ArgumentError, "There should be either no how/what arguments, or two (one how, one what); got #{howwhat.length}: #{howwhat.inspect}"
|
28
|
+
end
|
29
|
+
ObjectSpace.each_object(Class) do |klass|
|
30
|
+
if klass < curr_klass
|
31
|
+
Vapir::ElementObjectCandidates.match_candidates([element_object], klass.specifiers, klass.all_dom_attr_aliases) do |match|
|
32
|
+
curr_klass=klass
|
33
|
+
break
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
curr_klass.new(how, what, extra.merge(:element_object => element_object, :locate => false))
|
38
|
+
end
|
39
|
+
|
40
|
+
# takes any number of arguments, where each argument is either:
|
41
|
+
# - a symbol or strings representing a method that is the same in ruby and on the dom
|
42
|
+
# - or a hash of key/value pairs where each key is a dom attribute, and each value
|
43
|
+
# is a is a corresponding ruby method name or list of ruby method names.
|
44
|
+
def dom_attr(*dom_attrs)
|
45
|
+
dom_attrs.each do |arg|
|
46
|
+
hash=arg.is_a?(Hash) ? arg : arg.is_a?(Symbol) || arg.is_a?(String) ? {arg => arg} : raise(ArgumentError, "don't know what to do with arg #{arg.inspect} (#{arg.class})")
|
47
|
+
hash.each_pair do |dom_attr, ruby_method_names|
|
48
|
+
ruby_method_names= ruby_method_names.is_a?(Array) ? ruby_method_names : [ruby_method_names]
|
49
|
+
class_array_append 'dom_attrs', dom_attr
|
50
|
+
ruby_method_names.each do |ruby_method_name|
|
51
|
+
dom_attr_locate_alias(dom_attr, ruby_method_name)
|
52
|
+
define_method ruby_method_name do
|
53
|
+
method_from_element_object(dom_attr)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# creates aliases for locating by
|
61
|
+
def dom_attr_locate_alias(dom_attr, alias_name)
|
62
|
+
dom_attr_aliases=class_hash_get('dom_attr_aliases')
|
63
|
+
dom_attr_aliases[dom_attr] ||= Set.new
|
64
|
+
dom_attr_aliases[dom_attr] << alias_name
|
65
|
+
end
|
66
|
+
|
67
|
+
# dom_function is about the same as dom_attr, but dom_attr doesn't take arguments.
|
68
|
+
# also, dom_function methods call #wait; dom_attr ones don't.
|
69
|
+
def dom_function(*dom_functions)
|
70
|
+
dom_functions.each do |arg|
|
71
|
+
hash=arg.is_a?(Hash) ? arg : arg.is_a?(Symbol) || arg.is_a?(String) ? {arg => arg} : raise(ArgumentError, "don't know what to do with arg #{arg.inspect} (#{arg.class})")
|
72
|
+
hash.each_pair do |dom_function, ruby_method_names|
|
73
|
+
ruby_method_names= ruby_method_names.is_a?(Array) ? ruby_method_names : [ruby_method_names]
|
74
|
+
class_array_append 'dom_functions', dom_function
|
75
|
+
ruby_method_names.each do |ruby_method_name|
|
76
|
+
define_method ruby_method_name do |*args|
|
77
|
+
result=method_from_element_object(dom_function, *args)
|
78
|
+
wait
|
79
|
+
result
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# dom_setter takes arguments in the same format as dom_attr, but sends the given setter method (plus = sign)
|
87
|
+
# to the element object. eg,
|
88
|
+
# module TextField
|
89
|
+
# dom_setter :value
|
90
|
+
# dom_setter :maxLength => :maxlength
|
91
|
+
# end
|
92
|
+
# the #value= method in ruby will send to #value= on the element object
|
93
|
+
# the #maxlength= method in ruby will send to #maxLength= on the element object (note case difference).
|
94
|
+
def dom_setter(*dom_setters)
|
95
|
+
dom_setters.each do |arg|
|
96
|
+
hash=arg.is_a?(Hash) ? arg : arg.is_a?(Symbol) || arg.is_a?(String) ? {arg => arg} : raise(ArgumentError, "don't know what to do with arg #{arg.inspect} (#{arg.class})")
|
97
|
+
hash.each_pair do |dom_setter, ruby_method_names|
|
98
|
+
ruby_method_names= ruby_method_names.is_a?(Array) ? ruby_method_names : [ruby_method_names]
|
99
|
+
class_array_append 'dom_setters', dom_setter
|
100
|
+
ruby_method_names.each do |ruby_method_name|
|
101
|
+
define_method(ruby_method_name.to_s+'=') do |value|
|
102
|
+
element_object.send(dom_setter.to_s+'=', value)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# defines an element collection method on the given element - such as SelectList#options
|
110
|
+
# or Table#rows. takes the name of the dom method that returns a collection
|
111
|
+
# of element objects, a ruby method name, and an element class - actually this is
|
112
|
+
# generally an Element module; this method goes ahead and finds the browser-specific
|
113
|
+
# class that will actually be instantiated. the defined method returns an
|
114
|
+
# ElementCollection.
|
115
|
+
def element_collection(dom_attr, ruby_method_name, element_class)
|
116
|
+
define_method ruby_method_name do
|
117
|
+
assert_exists do
|
118
|
+
ElementCollection.new(self, element_class_for(element_class), extra_for_contained.merge(:candidates => dom_attr))
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# notes the given arguments to be inspected by #inspect and #to_s on each inheriting element.
|
124
|
+
# each argument may be a symbol, in which case the corresponding method is called on the element, or
|
125
|
+
# a hash, with the following keys:
|
126
|
+
# - :label - how the the attribute is labeled in the string returned by #inspect or #to_s.
|
127
|
+
# should be a string or symbol (but anything works; #to_s is called on the label).
|
128
|
+
# - :value - can be one of:
|
129
|
+
# - String starting with '@' - assumes this is an instance variable; gets the value of that instance variable
|
130
|
+
# - Symbol - assumes it is a method name, gives this to #send on the element. this is most commonly-used.
|
131
|
+
# - Proc - calls the proc, giving this element as an argument. should return a string. #to_s is called on its return value.
|
132
|
+
# - anything else - just assumes that that is the value that is wanted in the string.
|
133
|
+
# (see Element#attributes_for_stringifying)
|
134
|
+
# - :if - if defined, should be a proc that returns false/nil if this should not be included in the
|
135
|
+
# string, or anything else (that is, any value considered 'true') if it should. this element is passed
|
136
|
+
# as an argument to the proc.
|
137
|
+
def inspect_these(*inspect_these)
|
138
|
+
inspect_these.each do |inspect_this|
|
139
|
+
attribute_to_inspect=case inspect_this
|
140
|
+
when Hash
|
141
|
+
inspect_this
|
142
|
+
when Symbol
|
143
|
+
{:label => inspect_this, :value => inspect_this}
|
144
|
+
else
|
145
|
+
raise ArgumentError, "unrecognized thing to inspect: #{inspect_this} (#{inspect_this.class})"
|
146
|
+
end
|
147
|
+
class_array_append 'attributes_to_inspect', attribute_to_inspect
|
148
|
+
end
|
149
|
+
end
|
150
|
+
alias inspect_this inspect_these
|
151
|
+
# inspect_this_if(inspect_this, &block) is shorthand for
|
152
|
+
# inspect_this({:label => inspect_this, :value => inspect_this, :if => block)
|
153
|
+
# if a block isn't given, the :if proc is the result of sending the inspect_this symbol to the element.
|
154
|
+
# if inspect_this isn't a symbol, and no block is given, raises ArgumentError.
|
155
|
+
def inspect_this_if inspect_this, &block
|
156
|
+
unless inspect_this.is_a?(Symbol) || block
|
157
|
+
raise ArgumentError, "Either give a block, or specify a symbol as the first argument, instead of #{inspect_this.inspect} (#{inspect_this.class})"
|
158
|
+
end
|
159
|
+
to_inspect={:label => inspect_this, :value => inspect_this}
|
160
|
+
to_inspect[:if]= block || proc {|element| element.send(inspect_this) }
|
161
|
+
class_array_append 'attributes_to_inspect', to_inspect
|
162
|
+
end
|
163
|
+
|
164
|
+
def class_array_append(name, *elements)
|
165
|
+
=begin
|
166
|
+
name='@@'+name.to_s
|
167
|
+
unless self.class_variable_defined?(name)
|
168
|
+
class_variable_set(name, [])
|
169
|
+
end
|
170
|
+
class_variable_get(name).push(*elements)
|
171
|
+
=end
|
172
|
+
name=name.to_s.capitalize
|
173
|
+
unless self.const_defined?(name)
|
174
|
+
self.const_set(name, [])
|
175
|
+
end
|
176
|
+
self.const_get(name).push(*elements)
|
177
|
+
end
|
178
|
+
|
179
|
+
def class_array_get(name)
|
180
|
+
# just return the value of appending nothing
|
181
|
+
class_array_append(name)
|
182
|
+
end
|
183
|
+
def class_hash_merge(name, hash)
|
184
|
+
name=name.to_s.capitalize
|
185
|
+
unless self.const_defined?(name)
|
186
|
+
self.const_set(name, {})
|
187
|
+
end
|
188
|
+
self.const_get(name).merge!(hash)
|
189
|
+
end
|
190
|
+
def class_hash_get(name)
|
191
|
+
class_hash_merge(name, {})
|
192
|
+
end
|
193
|
+
def set_or_get_class_var(class_var, *arg)
|
194
|
+
if arg.length==0
|
195
|
+
class_variable_defined?(class_var) ? class_variable_get(class_var) : nil
|
196
|
+
elsif arg.length==1
|
197
|
+
class_variable_set(class_var, arg.first)
|
198
|
+
else
|
199
|
+
raise ArgumentError, "#{arg.length} arguments given; expected one or two. arguments were #{arg.inspect}"
|
200
|
+
end
|
201
|
+
end
|
202
|
+
def default_how(*arg)
|
203
|
+
set_or_get_class_var('@@default_how', *arg)
|
204
|
+
end
|
205
|
+
def add_container_method_extra_args(*args)
|
206
|
+
class_array_append('container_method_extra_args', *args)
|
207
|
+
end
|
208
|
+
def container_method_extra_args
|
209
|
+
class_array_get('container_method_extra_args')
|
210
|
+
end
|
211
|
+
def specifiers
|
212
|
+
class_array_get 'specifiers'
|
213
|
+
end
|
214
|
+
def container_single_methods
|
215
|
+
class_array_get 'container_single_methods'
|
216
|
+
end
|
217
|
+
def container_collection_methods
|
218
|
+
class_array_get 'container_collection_methods'
|
219
|
+
end
|
220
|
+
|
221
|
+
def parent_element_module(*arg)
|
222
|
+
defined_parent=set_or_get_class_var('@@parent_element_module', *arg)
|
223
|
+
defined_parent || (self==Watir::Element ? nil : Watir::Element)
|
224
|
+
end
|
225
|
+
def all_dom_attrs
|
226
|
+
super_attrs= parent_element_module ? parent_element_module.all_dom_attrs : []
|
227
|
+
super_attrs + class_array_get('dom_attrs')
|
228
|
+
end
|
229
|
+
def all_dom_attr_aliases
|
230
|
+
aliases=class_hash_get('dom_attr_aliases').dup
|
231
|
+
super_aliases= parent_element_module ? parent_element_module.all_dom_attr_aliases : {}
|
232
|
+
super_aliases.each_pair do |attr, alias_list|
|
233
|
+
aliases[attr] = (aliases[attr] || Set.new) + alias_list
|
234
|
+
end
|
235
|
+
aliases
|
236
|
+
end
|
237
|
+
end
|
238
|
+
module ElementHelper
|
239
|
+
def add_specifier(specifier)
|
240
|
+
class_array_append 'specifiers', specifier
|
241
|
+
end
|
242
|
+
|
243
|
+
def container_single_method(*method_names)
|
244
|
+
class_array_append 'container_single_methods', *method_names
|
245
|
+
element_module=self
|
246
|
+
method_names.each do |method_name|
|
247
|
+
Vapir::Element.module_eval do
|
248
|
+
# these methods (Element#parent_table, Element#parent_div, etc)
|
249
|
+
# iterate through parent nodes looking for a parent of the specified
|
250
|
+
# type. if no element of that type is found which is a parent of
|
251
|
+
# self, returns nil.
|
252
|
+
define_method("parent_#{method_name}") do
|
253
|
+
element_class=element_class_for(element_module)
|
254
|
+
parentNode=element_object
|
255
|
+
while true
|
256
|
+
parentNode=parentNode.parentNode
|
257
|
+
unless parentNode && parentNode != document_object # don't ascend up to the document. #TODO/Fix - for IE, comparing WIN32OLEs doesn't really work, this comparison is pointless.
|
258
|
+
return nil
|
259
|
+
end
|
260
|
+
matched=Vapir::ElementObjectCandidates.match_candidates([parentNode], element_class.specifiers, element_class.all_dom_attr_aliases)
|
261
|
+
if matched.size > 0
|
262
|
+
return element_class.new(:element_object, parentNode, extra_for_contained) # this is a little weird, passing extra_for_contained so that this is the container of its parent.
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
element_module = self
|
268
|
+
# define both bang-methods (like #text_field!) and not (#text_field) with corresponding :locate option for element_by_howwhat
|
269
|
+
[ {:method_name => method_name, :locate => true},
|
270
|
+
{:method_name => method_name.to_s+'!', :locate => :assert},
|
271
|
+
{:method_name => method_name.to_s+'?', :locate => :nil_unless_exists},
|
272
|
+
].each do |method_hash|
|
273
|
+
Vapir::Container.module_eval do
|
274
|
+
define_method(method_hash[:method_name]) do |*args| # can't take how, what as args because blocks don't do default values so it will want 2 args
|
275
|
+
#locate! # make sure self is located before trying contained stuff
|
276
|
+
how=args.shift
|
277
|
+
what=args.shift
|
278
|
+
what_args=args
|
279
|
+
other_attribute_keys=element_class_for(element_module).container_method_extra_args
|
280
|
+
if what_args.size>other_attribute_keys.length
|
281
|
+
raise ArgumentError, "\##{method_hash[:method_name]} takes 1 to #{2+other_attribute_keys.length} arguments! Got #{([how, what]+what_args).map{|a|a.inspect}.join(', ')}}"
|
282
|
+
end
|
283
|
+
if what_args.size == 0
|
284
|
+
other_attributes= nil
|
285
|
+
else
|
286
|
+
other_attributes={}
|
287
|
+
what_args.each_with_index do |arg, i|
|
288
|
+
other_attributes[other_attribute_keys[i]]=arg
|
289
|
+
end
|
290
|
+
end
|
291
|
+
element_by_howwhat(element_class_for(element_module), how, what, :locate => method_hash[:locate], :other_attributes => other_attributes)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
def container_collection_method(*method_names)
|
298
|
+
class_array_append 'container_collection_methods', *method_names
|
299
|
+
element_module=self
|
300
|
+
method_names.each do |container_multiple_method|
|
301
|
+
Vapir::Container.module_eval do
|
302
|
+
# returns an ElementCollection of Elements that are instances of the including class
|
303
|
+
define_method(container_multiple_method) do |*args|
|
304
|
+
case args.length
|
305
|
+
when 0
|
306
|
+
ElementCollection.new(self, element_class_for(element_module), extra_for_contained)
|
307
|
+
when 1,2
|
308
|
+
first, second=*args
|
309
|
+
how, what, index= *normalize_how_what_index(first, second, element_class_for(element_module))
|
310
|
+
if index
|
311
|
+
raise ArgumentError, "Cannot specify index on collection method! specified index was #{index.inspect}"
|
312
|
+
end
|
313
|
+
ElementCollection.new(self, element_class_for(element_module), extra_for_contained, how, what)
|
314
|
+
else
|
315
|
+
raise ArgumentError, "wrong number of arguments - expected 0 arguments, 1 argument (hash of attributes), or 2 arguments ('how' and 'what'). got #{args.length}: #{args.inspect}"
|
316
|
+
end
|
317
|
+
end
|
318
|
+
define_method('child_'+container_multiple_method.to_s) do
|
319
|
+
ElementCollection.new(self, element_class_for(element_module), extra_for_contained.merge(:candidates => :childNodes))
|
320
|
+
end
|
321
|
+
define_method('show_'+container_multiple_method.to_s) do |*io|
|
322
|
+
io=io.first||$stdout # io is a *array so that you don't have to give an arg (since procs don't do default args)
|
323
|
+
element_collection=ElementCollection.new(self, element_class_for(element_module), extra_for_contained)
|
324
|
+
io.write("There are #{element_collection.length} #{container_multiple_method}\n")
|
325
|
+
element_collection.each do |element|
|
326
|
+
io.write(element.to_s)
|
327
|
+
end
|
328
|
+
end
|
329
|
+
alias_deprecated "show#{container_multiple_method.to_s.capitalize}", "show_"+container_multiple_method.to_s
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
include ElementClassAndModuleMethods
|
335
|
+
|
336
|
+
def included(including_class)
|
337
|
+
including_class.send :extend, ElementClassAndModuleMethods
|
338
|
+
|
339
|
+
# copy constants (like Specifiers) onto classes when inherited
|
340
|
+
# this is here to set the constants of the Element modules below onto the actual classes that instantiate
|
341
|
+
# per-browser (Vapir::IE::TextField, Vapir::Firefox::TextField, etc) so that calling #const_defined? on those
|
342
|
+
# returns true, and so that the constants defined here clobber any inherited stuff from superclasses
|
343
|
+
# which is unwanted.
|
344
|
+
self.constants.each do |const| # copy all of its constants onto wherever it was included
|
345
|
+
to_copy=self.const_get(const)
|
346
|
+
to_copy=to_copy.dup if [Hash, Array, Set].any?{|klass| to_copy.is_a?(klass) }
|
347
|
+
including_class.const_set(const, to_copy)
|
348
|
+
end
|
349
|
+
|
350
|
+
# now the constants (above) have switched away from constants to class variables, pretty much, so copy those.
|
351
|
+
self.class_variables.each do |class_var|
|
352
|
+
to_copy=class_variable_get(class_var)
|
353
|
+
to_copy=to_copy.dup if [Hash, Array, Set].any?{|klass| to_copy.is_a?(klass) }
|
354
|
+
including_class.send(:class_variable_set, class_var, to_copy)
|
355
|
+
end
|
356
|
+
|
357
|
+
class << including_class
|
358
|
+
def attributes_to_inspect
|
359
|
+
super_attrs=superclass.respond_to?(:attributes_to_inspect) ? superclass.attributes_to_inspect : []
|
360
|
+
super_attrs + class_array_get('attributes_to_inspect')
|
361
|
+
end
|
362
|
+
def all_dom_attrs
|
363
|
+
super_attrs=superclass.respond_to?(:all_dom_attrs) ? superclass.all_dom_attrs : []
|
364
|
+
super_attrs + class_array_get('dom_attrs')
|
365
|
+
end
|
366
|
+
def all_dom_attr_aliases
|
367
|
+
aliases=class_hash_get('dom_attr_aliases').dup
|
368
|
+
super_aliases=superclass.respond_to?(:all_dom_attr_aliases) ? superclass.all_dom_attr_aliases : {}
|
369
|
+
super_aliases.each_pair do |attr, alias_list|
|
370
|
+
aliases[attr] = (aliases[attr] || Set.new) + alias_list
|
371
|
+
end
|
372
|
+
aliases
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
end
|
378
|
+
end
|
@@ -4,35 +4,46 @@ module Vapir
|
|
4
4
|
class ElementCollection
|
5
5
|
include Enumerable
|
6
6
|
|
7
|
-
def initialize(container, collection_class, extra={})
|
7
|
+
def initialize(container, collection_class, extra={}, how=nil, what=nil)
|
8
8
|
@container=container
|
9
9
|
@collection_class=collection_class
|
10
10
|
@extra=extra.merge(:container => container)
|
11
|
+
@how=how
|
12
|
+
@what=what
|
11
13
|
end
|
12
14
|
|
13
|
-
|
14
|
-
|
15
|
-
|
15
|
+
# yields each element in the collection to the given block
|
16
|
+
def each # :yields: element
|
17
|
+
element_objects.each do |element_object|
|
18
|
+
# todo: instantiated Element should use @how/@what? that would make #each the same as with #each_by_index
|
19
|
+
yield @collection_class.new(:element_object, element_object, @extra)
|
16
20
|
end
|
17
21
|
self
|
18
22
|
end
|
19
|
-
|
23
|
+
# yields each index from 1..length.
|
24
|
+
#
|
25
|
+
# note that if you are using this and accessing each subscript, it will be exponentially slower
|
26
|
+
# than using #each or #each_with_index.
|
27
|
+
def each_index # :yields: index
|
20
28
|
(1..length).each do |i|
|
21
29
|
yield i
|
22
30
|
end
|
23
31
|
end
|
32
|
+
# returns the number of elements in the collection
|
24
33
|
def length
|
25
|
-
|
34
|
+
element_objects.length
|
26
35
|
end
|
27
36
|
alias size length
|
37
|
+
# returns true if this collection contains no elements
|
28
38
|
def empty?
|
29
39
|
size==0
|
30
40
|
end
|
31
41
|
alias each_with_enumerable_index each_with_index # call ruby's 0-based indexing enumerable_index; call ours element_index
|
32
|
-
|
42
|
+
# yields each element and index in the collection
|
43
|
+
def each_with_element_index # :yields: element, index
|
33
44
|
index=1
|
34
|
-
|
35
|
-
yield @collection_class.new(
|
45
|
+
element_objects.each do |element_object|
|
46
|
+
yield @collection_class.new(@how, @what, @extra.merge(:index => index, :element_object => element_object, :locate => false)), index
|
36
47
|
index+=1
|
37
48
|
end
|
38
49
|
self
|
@@ -46,38 +57,115 @@ module Vapir
|
|
46
57
|
yield element
|
47
58
|
end
|
48
59
|
end
|
60
|
+
def by_index
|
61
|
+
Enumerator.new(self, :each_by_index)
|
62
|
+
end
|
49
63
|
# returns the element at the given index in the collection. indices start at 1.
|
50
64
|
def [](index)
|
51
65
|
at(index)
|
52
66
|
end
|
67
|
+
# returns the element at the given index in the collection. indices start at 1.
|
53
68
|
def at(index)
|
54
|
-
@collection_class.new(
|
69
|
+
@collection_class.new(@how, @what, @extra.merge(:index => index))
|
55
70
|
end
|
71
|
+
# returns the first element in the collection.
|
56
72
|
def first
|
57
73
|
at(:first)
|
58
74
|
end
|
75
|
+
# returns the last element. this will refer to the last element even if the number of elements changes, assuming relocation.
|
59
76
|
def last
|
60
77
|
at(:last)
|
61
78
|
end
|
62
79
|
|
63
|
-
|
64
|
-
|
65
|
-
|
80
|
+
alias enumerable_select select
|
81
|
+
def select(&block) # :yields: element
|
82
|
+
# TODO: test
|
83
|
+
if @how
|
84
|
+
enumerable_select(&block)
|
85
|
+
else
|
86
|
+
ElementCollection.new(@container, @collection_class, @extra, :custom, block)
|
87
|
+
end
|
66
88
|
end
|
67
|
-
alias detect find
|
68
89
|
|
69
|
-
|
70
|
-
|
90
|
+
alias enumerable_reject reject
|
91
|
+
def reject(&block) # :yields: element
|
92
|
+
# TODO: test
|
93
|
+
if @how
|
94
|
+
enumerable_reject(&block)
|
95
|
+
else
|
96
|
+
ElementCollection.new(@container, @collection_class, @extra, :custom, proc{|el| !block.call(el) })
|
97
|
+
end
|
71
98
|
end
|
72
|
-
|
99
|
+
|
100
|
+
alias enumerable_find find
|
101
|
+
# returns an element for which the given block returns true (that is, not false or nil) when yielded that element
|
102
|
+
#
|
103
|
+
# returns nil if no such element exists.
|
104
|
+
def find(&block) # :yields: element
|
105
|
+
if @how # can't set how=:custom if @how is given to us, so fall back to Enumerable's #find method
|
106
|
+
enumerable_find(&block)
|
107
|
+
else
|
108
|
+
element=@collection_class.new(:custom, block, @extra.merge(:locate => false))
|
109
|
+
element.exists? ? element : nil
|
110
|
+
end
|
111
|
+
end
|
112
|
+
alias detect find
|
113
|
+
# returns an element for which the given block returns true (that is, not false or nil) when yielded that element
|
114
|
+
#
|
115
|
+
# raises UnknownObjectException if no such element exists.
|
116
|
+
def find!(&block)
|
117
|
+
if @how # can't set how=:custom if @how is given to us, so fall back to Enumerable's #find method
|
118
|
+
enumerable_find(&block) || begin
|
119
|
+
# TODO: DRY against Element#locate!
|
120
|
+
klass=(@collection_class <= Frame) ? Vapir::Exception::UnknownFrameException : Vapir::Exception::UnknownObjectException
|
121
|
+
message="Unable to locate #{@collection_class} using custom find block"
|
122
|
+
message+="\non element collection #{self.inspect}"
|
123
|
+
message+="\non container: #{@container.inspect}"
|
124
|
+
raise(klass, message)
|
125
|
+
end
|
126
|
+
else
|
127
|
+
element=@collection_class.new(:custom, block, @extra.merge(:locate => :assert))
|
128
|
+
end
|
129
|
+
end
|
130
|
+
alias detect! find!
|
131
|
+
|
73
132
|
private
|
74
133
|
include ElementObjectCandidates
|
75
|
-
def
|
134
|
+
def element_objects
|
135
|
+
# TODO: this is heavily redundant with Element#locate; DRY
|
76
136
|
assert_container_exists
|
77
|
-
|
137
|
+
case @how
|
138
|
+
when nil
|
139
|
+
matched_candidates(@collection_class.specifiers, @collection_class.all_dom_attr_aliases)
|
140
|
+
when :xpath
|
141
|
+
unless @container.respond_to?(:element_objects_by_xpath)
|
142
|
+
raise Vapir::Exception::MissingWayOfFindingObjectException, "Locating by xpath is not supported on the container #{@container.inspect}"
|
143
|
+
end
|
144
|
+
by_xpath=@container.element_objects_by_xpath(@what)
|
145
|
+
match_candidates(by_xpath, @collection_class.specifiers, @collection_class.all_dom_attr_aliases)
|
146
|
+
when :css
|
147
|
+
assert_container_exists
|
148
|
+
match_candidates(@container.containing_object.querySelectorAll(@what), @collection_class.specifiers, @collection_class.all_dom_attr_aliases)
|
149
|
+
when :attributes
|
150
|
+
specified_attributes=@what
|
151
|
+
specifiers=@collection_class.specifiers.map{|spec| spec.merge(specified_attributes)}
|
152
|
+
|
153
|
+
matched_candidates(specifiers, @collection_class.all_dom_attr_aliases)
|
154
|
+
when :custom
|
155
|
+
matched_candidates(@collection_class.specifiers, @collection_class.all_dom_attr_aliases).select do |candidate|
|
156
|
+
@what.call(@collection_class.new(:element_object, candidate, @extra))
|
157
|
+
end
|
158
|
+
else
|
159
|
+
raise Vapir::Exception::MissingWayOfFindingObjectException, "Unknown 'how' given: #{@how.inspect} (#{@how.class}). 'what' was #{@what.inspect} (#{@what.class})"
|
160
|
+
end
|
78
161
|
end
|
79
162
|
public
|
80
|
-
def
|
163
|
+
def inspect # :nodoc:
|
164
|
+
# todo: include how/what if set
|
165
|
+
"\#<#{self.class.name}:0x#{"%.8x"%(self.hash*2)} #{map{|el|el.inspect}.join(', ')}>"
|
166
|
+
end
|
167
|
+
def pretty_print(pp) # :nodoc:
|
168
|
+
# todo: include how/what if set
|
81
169
|
pp.object_address_group(self) do
|
82
170
|
pp.seplist(self, lambda { pp.text ',' }) do |element|
|
83
171
|
pp.breakable ' '
|