vapir-common 1.7.2 → 1.8.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 ' '
|