oki-celerity 0.8.1.dev
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY +111 -0
- data/LICENSE +621 -0
- data/README.rdoc +80 -0
- data/Rakefile +11 -0
- data/VERSION.yml +5 -0
- data/celerity.gemspec +126 -0
- data/lib/celerity/browser.rb +908 -0
- data/lib/celerity/clickable_element.rb +73 -0
- data/lib/celerity/collections.rb +164 -0
- data/lib/celerity/container.rb +800 -0
- data/lib/celerity/default_viewer.rb +14 -0
- data/lib/celerity/disabled_element.rb +40 -0
- data/lib/celerity/element.rb +311 -0
- data/lib/celerity/element_collection.rb +107 -0
- data/lib/celerity/element_locator.rb +164 -0
- data/lib/celerity/elements/button.rb +54 -0
- data/lib/celerity/elements/file_field.rb +29 -0
- data/lib/celerity/elements/form.rb +22 -0
- data/lib/celerity/elements/frame.rb +86 -0
- data/lib/celerity/elements/image.rb +89 -0
- data/lib/celerity/elements/label.rb +16 -0
- data/lib/celerity/elements/link.rb +43 -0
- data/lib/celerity/elements/meta.rb +14 -0
- data/lib/celerity/elements/non_control_elements.rb +124 -0
- data/lib/celerity/elements/option.rb +38 -0
- data/lib/celerity/elements/radio_check.rb +114 -0
- data/lib/celerity/elements/select_list.rb +146 -0
- data/lib/celerity/elements/table.rb +153 -0
- data/lib/celerity/elements/table_cell.rb +36 -0
- data/lib/celerity/elements/table_elements.rb +42 -0
- data/lib/celerity/elements/table_row.rb +49 -0
- data/lib/celerity/elements/text_field.rb +168 -0
- data/lib/celerity/exception.rb +83 -0
- data/lib/celerity/htmlunit/apache-mime4j-0.6.jar +0 -0
- data/lib/celerity/htmlunit/commons-codec-1.4.jar +0 -0
- data/lib/celerity/htmlunit/commons-collections-3.2.1.jar +0 -0
- data/lib/celerity/htmlunit/commons-io-1.4.jar +0 -0
- data/lib/celerity/htmlunit/commons-lang-2.5.jar +0 -0
- data/lib/celerity/htmlunit/commons-logging-1.1.1.jar +0 -0
- data/lib/celerity/htmlunit/cssparser-0.9.5.jar +0 -0
- data/lib/celerity/htmlunit/htmlunit-2.9-SNAPSHOT.jar +0 -0
- data/lib/celerity/htmlunit/htmlunit-core-js-2.8.jar +0 -0
- data/lib/celerity/htmlunit/httpclient-4.0.1.jar +0 -0
- data/lib/celerity/htmlunit/httpcore-4.0.1.jar +0 -0
- data/lib/celerity/htmlunit/httpmime-4.0.1.jar +0 -0
- data/lib/celerity/htmlunit/nekohtml-1.9.14.jar +0 -0
- data/lib/celerity/htmlunit/sac-1.3.jar +0 -0
- data/lib/celerity/htmlunit/serializer-2.7.1.jar +0 -0
- data/lib/celerity/htmlunit/xalan-2.7.1.jar +0 -0
- data/lib/celerity/htmlunit/xercesImpl-2.9.1.jar +0 -0
- data/lib/celerity/htmlunit/xml-apis-1.3.04.jar +0 -0
- data/lib/celerity/htmlunit.rb +64 -0
- data/lib/celerity/identifier.rb +28 -0
- data/lib/celerity/ignoring_web_connection.rb +15 -0
- data/lib/celerity/input_element.rb +25 -0
- data/lib/celerity/javascript_debugger.rb +32 -0
- data/lib/celerity/listener.rb +143 -0
- data/lib/celerity/resources/no_viewer.png +0 -0
- data/lib/celerity/short_inspect.rb +20 -0
- data/lib/celerity/util.rb +129 -0
- data/lib/celerity/version.rb +3 -0
- data/lib/celerity/viewer_connection.rb +89 -0
- data/lib/celerity/watir_compatibility.rb +70 -0
- data/lib/celerity/xpath_support.rb +52 -0
- data/lib/celerity.rb +77 -0
- data/tasks/benchmark.rake +4 -0
- data/tasks/check.rake +24 -0
- data/tasks/clean.rake +3 -0
- data/tasks/fix.rake +25 -0
- data/tasks/jar.rake +55 -0
- data/tasks/jeweler.rake +28 -0
- data/tasks/rdoc.rake +4 -0
- data/tasks/snapshot.rake +25 -0
- data/tasks/spec.rake +26 -0
- data/tasks/website.rake +10 -0
- data/tasks/yard.rake +16 -0
- metadata +204 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
module Celerity
|
2
|
+
|
3
|
+
#
|
4
|
+
# Mixed in to all elements that can have the 'disabled' attribute.
|
5
|
+
#
|
6
|
+
|
7
|
+
module DisabledElement
|
8
|
+
include Celerity::Exception
|
9
|
+
|
10
|
+
#
|
11
|
+
# Returns false if the element is disabled.
|
12
|
+
#
|
13
|
+
|
14
|
+
def enabled?
|
15
|
+
!disabled?
|
16
|
+
end
|
17
|
+
|
18
|
+
#
|
19
|
+
# Returns true if the element is disabled.
|
20
|
+
#
|
21
|
+
|
22
|
+
def disabled?
|
23
|
+
assert_exists unless defined?(@object) && @object
|
24
|
+
@object.isDisabled
|
25
|
+
end
|
26
|
+
alias_method :disabled, :disabled?
|
27
|
+
|
28
|
+
#
|
29
|
+
# Used internally.
|
30
|
+
# @api private
|
31
|
+
#
|
32
|
+
|
33
|
+
def assert_enabled
|
34
|
+
if disabled?
|
35
|
+
raise ObjectDisabledException, "Object #{identifier_string} is disabled"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,311 @@
|
|
1
|
+
module Celerity
|
2
|
+
|
3
|
+
#
|
4
|
+
# Superclass for all HTML elements.
|
5
|
+
#
|
6
|
+
|
7
|
+
class Element
|
8
|
+
include Exception
|
9
|
+
include Container
|
10
|
+
|
11
|
+
attr_reader :container
|
12
|
+
|
13
|
+
# HTML 4.01 Transitional DTD
|
14
|
+
HTML_401_TRANSITIONAL = {
|
15
|
+
:core => [:class, :id, :style, :title],
|
16
|
+
:cell_halign => [:align, :char, :charoff],
|
17
|
+
:cell_valign => [:valign],
|
18
|
+
:i18n => [:dir, :lang],
|
19
|
+
:event => [:onclick, :ondblclick, :onmousedown, :onmouseup, :onmouseover,
|
20
|
+
:onmousemove, :onmouseout, :onkeypress, :onkeydown, :onkeyup],
|
21
|
+
:sloppy => [:name, :value]
|
22
|
+
}
|
23
|
+
|
24
|
+
CELLHALIGN_ATTRIBUTES = HTML_401_TRANSITIONAL[:cell_halign]
|
25
|
+
CELLVALIGN_ATTRIBUTES = HTML_401_TRANSITIONAL[:cell_valign]
|
26
|
+
BASE_ATTRIBUTES = HTML_401_TRANSITIONAL.values_at(:core, :i18n, :event, :sloppy).flatten
|
27
|
+
ATTRIBUTES = BASE_ATTRIBUTES
|
28
|
+
TAGS = []
|
29
|
+
|
30
|
+
DEFAULT_HOW = nil
|
31
|
+
|
32
|
+
# @api private
|
33
|
+
def initialize(container, *args)
|
34
|
+
self.container = container
|
35
|
+
|
36
|
+
case args.size
|
37
|
+
when 2
|
38
|
+
@conditions = { args[0] => args[1] }
|
39
|
+
when 1
|
40
|
+
if args.first.is_a? Hash
|
41
|
+
@conditions = args.first
|
42
|
+
elsif (how = self.class::DEFAULT_HOW)
|
43
|
+
@conditions = { how => args.first }
|
44
|
+
else
|
45
|
+
raise ArgumentError, "wrong number of arguments (1 for 2)"
|
46
|
+
end
|
47
|
+
when 0
|
48
|
+
@conditions = { :index => Celerity.index_offset }
|
49
|
+
else
|
50
|
+
raise ArgumentError, "wrong number of arguments (#{args.size} for 2)"
|
51
|
+
end
|
52
|
+
|
53
|
+
@conditions.freeze
|
54
|
+
@object = nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def ==(other)
|
58
|
+
return false unless other.kind_of? Element
|
59
|
+
xpath == other.xpath
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
# Get the parent element
|
64
|
+
# @return [Celerity::Element, nil] subclass of Celerity::Element, or nil if no parent was found
|
65
|
+
#
|
66
|
+
|
67
|
+
def parent
|
68
|
+
assert_exists
|
69
|
+
|
70
|
+
obj = @object.parentNode
|
71
|
+
until element_class = Celerity::Util.htmlunit2celerity(obj.class)
|
72
|
+
return nil if obj.nil?
|
73
|
+
obj = obj.parentNode
|
74
|
+
end
|
75
|
+
|
76
|
+
element_class.new(@container, :object, obj)
|
77
|
+
end
|
78
|
+
|
79
|
+
#
|
80
|
+
# Sets the focus to this element.
|
81
|
+
#
|
82
|
+
|
83
|
+
def focus
|
84
|
+
assert_exists
|
85
|
+
@object.focus
|
86
|
+
end
|
87
|
+
|
88
|
+
#
|
89
|
+
# Returns true if this element is the currently focused element
|
90
|
+
#
|
91
|
+
# Celerity-specific.
|
92
|
+
#
|
93
|
+
|
94
|
+
def focused?
|
95
|
+
assert_exists
|
96
|
+
@object == browser.page.getFocusedElement
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# Fires the given event for this element
|
101
|
+
#
|
102
|
+
|
103
|
+
def fire_event(event_name)
|
104
|
+
assert_exists
|
105
|
+
@object.fireEvent(event_name.sub(/^on/, ''))
|
106
|
+
end
|
107
|
+
|
108
|
+
#
|
109
|
+
# Used internally. Find the element on the page.
|
110
|
+
# @api private
|
111
|
+
#
|
112
|
+
|
113
|
+
def locate
|
114
|
+
@object = ElementLocator.new(@container, self.class).find_by_conditions(@conditions)
|
115
|
+
end
|
116
|
+
|
117
|
+
#
|
118
|
+
# Returns the HtmlUnit object backing this element
|
119
|
+
#
|
120
|
+
|
121
|
+
def object
|
122
|
+
@object || locate
|
123
|
+
end
|
124
|
+
|
125
|
+
#
|
126
|
+
# Returns a JavaScript object representing the receiver
|
127
|
+
#
|
128
|
+
# @api internal - subject to change
|
129
|
+
#
|
130
|
+
|
131
|
+
def javascript_object
|
132
|
+
assert_exists
|
133
|
+
@object.getScriptObject
|
134
|
+
end
|
135
|
+
|
136
|
+
#
|
137
|
+
# @return [String] A string representation of the element.
|
138
|
+
#
|
139
|
+
|
140
|
+
def to_s
|
141
|
+
assert_exists
|
142
|
+
Celerity::Util.create_string @object
|
143
|
+
end
|
144
|
+
|
145
|
+
#
|
146
|
+
# @param [String, #to_s] The attribute.
|
147
|
+
# @return [String] The value of the given attribute.
|
148
|
+
#
|
149
|
+
|
150
|
+
def attribute_value(attribute)
|
151
|
+
assert_exists
|
152
|
+
@object.getAttribute attribute.to_s
|
153
|
+
end
|
154
|
+
|
155
|
+
#
|
156
|
+
# Check if the element is visible to the user or not.
|
157
|
+
# Note that this only takes the _style attribute_ of the element (and
|
158
|
+
# its parents) into account - styles from applied CSS is not considered.
|
159
|
+
#
|
160
|
+
# @return [boolean]
|
161
|
+
#
|
162
|
+
|
163
|
+
def visible?
|
164
|
+
assert_exists
|
165
|
+
@object.isDisplayed
|
166
|
+
end
|
167
|
+
|
168
|
+
#
|
169
|
+
# Used internally to ensure the element actually exists.
|
170
|
+
#
|
171
|
+
# @raise [Celerity::Exception::UnknownObjectException] if the element can't be found.
|
172
|
+
# @api private
|
173
|
+
#
|
174
|
+
|
175
|
+
def assert_exists
|
176
|
+
locate unless @object
|
177
|
+
unless @object
|
178
|
+
raise UnknownObjectException, "Unable to locate #{self.class.name[/::(.*)$/, 1]}, using #{identifier_string}"
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
#
|
183
|
+
# Checks if the element exists.
|
184
|
+
# @return [true, false]
|
185
|
+
#
|
186
|
+
|
187
|
+
def exists?
|
188
|
+
assert_exists
|
189
|
+
true
|
190
|
+
rescue UnknownObjectException, UnknownFrameException
|
191
|
+
false
|
192
|
+
end
|
193
|
+
alias_method :exist?, :exists?
|
194
|
+
|
195
|
+
#
|
196
|
+
# Return a text representation of the element as it would appear in a browser.
|
197
|
+
#
|
198
|
+
# @see inner_text
|
199
|
+
# @return [String]
|
200
|
+
#
|
201
|
+
|
202
|
+
def text
|
203
|
+
assert_exists
|
204
|
+
@object.asText.strip # this must behave like ElementLocator
|
205
|
+
end
|
206
|
+
|
207
|
+
#
|
208
|
+
# Return the text content of this DOM node, disregarding its visibility.
|
209
|
+
#
|
210
|
+
# (Celerity-specific?)
|
211
|
+
#
|
212
|
+
# @see text
|
213
|
+
# @return [String]
|
214
|
+
#
|
215
|
+
|
216
|
+
def inner_text
|
217
|
+
assert_exists
|
218
|
+
Celerity::Util.normalize_text @object.getTextContent
|
219
|
+
end
|
220
|
+
|
221
|
+
#
|
222
|
+
# @return [String] The normative XML representation of the element (including children).
|
223
|
+
#
|
224
|
+
|
225
|
+
def to_xml
|
226
|
+
assert_exists
|
227
|
+
@object.asXml
|
228
|
+
end
|
229
|
+
alias_method :asXml, :to_xml
|
230
|
+
alias_method :as_xml, :to_xml
|
231
|
+
alias_method :html, :to_xml
|
232
|
+
|
233
|
+
#
|
234
|
+
# @return [String] A string representation of the element's attributes.
|
235
|
+
#
|
236
|
+
|
237
|
+
def attribute_string
|
238
|
+
assert_exists
|
239
|
+
|
240
|
+
result = ''
|
241
|
+
@object.getAttributes.each do |attribute|
|
242
|
+
result << %Q{#{attribute.getName}="#{attribute.getValue}"}
|
243
|
+
end
|
244
|
+
|
245
|
+
result
|
246
|
+
end
|
247
|
+
|
248
|
+
#
|
249
|
+
# return the canonical xpath for this element (Celerity-specific)
|
250
|
+
#
|
251
|
+
|
252
|
+
def xpath
|
253
|
+
assert_exists
|
254
|
+
@object.getCanonicalXPath
|
255
|
+
end
|
256
|
+
|
257
|
+
#
|
258
|
+
# Dynamically get element attributes.
|
259
|
+
#
|
260
|
+
# @see ATTRIBUTES constant for a list of valid methods for a given element.
|
261
|
+
#
|
262
|
+
# @return [String] The resulting attribute.
|
263
|
+
# @raise [NoMethodError] if the element doesn't support this attribute.
|
264
|
+
#
|
265
|
+
|
266
|
+
def method_missing(meth, *args, &blk)
|
267
|
+
assert_exists
|
268
|
+
|
269
|
+
meth = selector_to_attribute(meth)
|
270
|
+
|
271
|
+
if self.class::ATTRIBUTES.include?(meth) || (self.class == Element && @object.hasAttribute(meth.to_s))
|
272
|
+
return @object.getAttribute(meth.to_s)
|
273
|
+
end
|
274
|
+
Log.warn "Element\#method_missing calling super with #{meth.inspect}"
|
275
|
+
super
|
276
|
+
end
|
277
|
+
|
278
|
+
def methods(*args)
|
279
|
+
ms = super
|
280
|
+
ms += self.class::ATTRIBUTES.map { |e| e.to_s }
|
281
|
+
ms.sort
|
282
|
+
end
|
283
|
+
|
284
|
+
def respond_to?(meth, include_private = false)
|
285
|
+
meth = selector_to_attribute(meth)
|
286
|
+
return true if self.class::ATTRIBUTES.include?(meth)
|
287
|
+
super
|
288
|
+
end
|
289
|
+
|
290
|
+
private
|
291
|
+
|
292
|
+
def identifier_string
|
293
|
+
if @conditions.size == 1
|
294
|
+
how, what = @conditions.to_a.first
|
295
|
+
"#{how.inspect} and #{what.inspect}"
|
296
|
+
else
|
297
|
+
@conditions.inspect
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
def selector_to_attribute(meth)
|
302
|
+
case meth
|
303
|
+
when :class_name then :class
|
304
|
+
when :caption then :value
|
305
|
+
when :url then :href
|
306
|
+
else meth
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
end # Element
|
311
|
+
end # Celerity
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Celerity
|
2
|
+
|
3
|
+
#
|
4
|
+
# This class is the superclass for the iterator classes (Buttons, Links, Spans etc.)
|
5
|
+
# It would normally only be accessed by the iterator methods (Browser#spans, Browser#links, ...).
|
6
|
+
#
|
7
|
+
|
8
|
+
class ElementCollection
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
#
|
12
|
+
# @api private
|
13
|
+
#
|
14
|
+
|
15
|
+
def initialize(container, how = nil, what = nil)
|
16
|
+
@container = container
|
17
|
+
@object = (how == :object ? what : nil)
|
18
|
+
@length = length
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# @return [Fixnum] The number of elements in this collection.
|
23
|
+
#
|
24
|
+
|
25
|
+
def length
|
26
|
+
if @object
|
27
|
+
@object.length
|
28
|
+
else
|
29
|
+
@elements ||= ElementLocator.new(@container, element_class).elements_by_idents
|
30
|
+
@elements.size
|
31
|
+
end
|
32
|
+
end
|
33
|
+
alias_method :size, :length
|
34
|
+
|
35
|
+
#
|
36
|
+
# @yieldparam [Celerity::Element] element Iterate through the elements in this collection.
|
37
|
+
#
|
38
|
+
|
39
|
+
def each
|
40
|
+
if @elements
|
41
|
+
@elements.each { |e| yield(element_class.new(@container, :object, e)) }
|
42
|
+
else
|
43
|
+
0.upto(@length - 1) { |i| yield iterator_object(i) }
|
44
|
+
end
|
45
|
+
|
46
|
+
@length
|
47
|
+
end
|
48
|
+
|
49
|
+
#
|
50
|
+
# Get the element at the given index.
|
51
|
+
# By default, this is 1-indexed to keep compatibility with Watir.
|
52
|
+
#
|
53
|
+
# Also note that because of Watir's lazy loading, this will return an Element
|
54
|
+
# instance even if the index is out of bounds.
|
55
|
+
#
|
56
|
+
# @param [Fixnum] n Index of wanted element, 1-indexed unless Celerity.index_offset is changed.
|
57
|
+
# @return [Celerity::Element] Returns a subclass of Celerity::Element
|
58
|
+
#
|
59
|
+
|
60
|
+
def [](n)
|
61
|
+
if @elements && @elements[n - Celerity.index_offset]
|
62
|
+
element_class.new(@container, :object, @elements[n - Celerity.index_offset])
|
63
|
+
else
|
64
|
+
iterator_object(n - Celerity.index_offset)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# Get the first element in this collection. (Celerity-specific)
|
70
|
+
#
|
71
|
+
# @return [Celerity::Element] Returns a subclass of Celerity::Element
|
72
|
+
#
|
73
|
+
|
74
|
+
def first
|
75
|
+
self[Celerity.index_offset]
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Get the last element in this collection. (Celerity-specific)
|
80
|
+
#
|
81
|
+
# @return [Celerity::Element] Returns a subclass of Celerity::Element
|
82
|
+
#
|
83
|
+
|
84
|
+
def last
|
85
|
+
self[Celerity.index_offset - 1]
|
86
|
+
end
|
87
|
+
|
88
|
+
#
|
89
|
+
# Note: This can be quite useful in irb:
|
90
|
+
#
|
91
|
+
# puts browser.text_fields
|
92
|
+
#
|
93
|
+
# @return [String] A string representation of all elements in this collection.
|
94
|
+
#
|
95
|
+
|
96
|
+
def to_s
|
97
|
+
map { |e| e.to_s }.join("\n")
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def iterator_object(i)
|
103
|
+
element_class.new(@container, :index, i + Celerity.index_offset)
|
104
|
+
end
|
105
|
+
|
106
|
+
end # ElementCollection
|
107
|
+
end # Celerity
|
@@ -0,0 +1,164 @@
|
|
1
|
+
module Celerity
|
2
|
+
|
3
|
+
#
|
4
|
+
# Used internally to locate elements on the page.
|
5
|
+
#
|
6
|
+
|
7
|
+
class ElementLocator
|
8
|
+
include Celerity::Exception
|
9
|
+
attr_accessor :idents
|
10
|
+
|
11
|
+
|
12
|
+
def initialize(container, element_class)
|
13
|
+
container.assert_exists
|
14
|
+
|
15
|
+
@container = container
|
16
|
+
@object = container.object
|
17
|
+
@element_class = element_class
|
18
|
+
@attributes = @element_class::ATTRIBUTES # could check for 'strict' here?
|
19
|
+
@idents = @element_class::TAGS
|
20
|
+
@tags = @idents.map { |e| e.tag.downcase }
|
21
|
+
end
|
22
|
+
|
23
|
+
def find_by_conditions(conditions) # TODO: refactor without performance hit
|
24
|
+
return nil unless @object # probably means we're on a TextPage (content-type is "text/plain")
|
25
|
+
|
26
|
+
@condition_idents = []
|
27
|
+
attributes = Hash.new { |h, k| h[k] = [] }
|
28
|
+
index = 0 # by default, return the first matching element
|
29
|
+
text = nil
|
30
|
+
|
31
|
+
conditions.each do |how, what|
|
32
|
+
case how
|
33
|
+
when :object
|
34
|
+
unless what.is_a?(HtmlUnit::Html::HtmlElement) || what.nil?
|
35
|
+
raise ArgumentError, "expected an HtmlUnit::Html::HtmlElement subclass, got #{what.inspect}:#{what.class}"
|
36
|
+
end
|
37
|
+
return what
|
38
|
+
when :xpath
|
39
|
+
return find_by_xpath(what)
|
40
|
+
when :label
|
41
|
+
return find_by_label(what) unless @attributes.include?(:label)
|
42
|
+
when :class_name
|
43
|
+
how = :class
|
44
|
+
when :url
|
45
|
+
how = :href
|
46
|
+
when :caption
|
47
|
+
how = :text
|
48
|
+
end
|
49
|
+
|
50
|
+
if how == :id && conditions.size == 1
|
51
|
+
return find_by_id(what)
|
52
|
+
elsif @attributes.include?(how = how.to_sym)
|
53
|
+
attributes[how] << what
|
54
|
+
elsif how == :index
|
55
|
+
index = what.to_i - Celerity.index_offset
|
56
|
+
elsif how == :text
|
57
|
+
text = what
|
58
|
+
else
|
59
|
+
raise MissingWayOfFindingObjectException, "No how #{how.inspect}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
@idents.each do |ident|
|
64
|
+
merged = attributes.merge(ident.attributes) { |key, v1, v2| v1 | v2 }
|
65
|
+
id = Identifier.new(ident.tag, merged)
|
66
|
+
id.text = ident.text || text # «original» identifier takes precedence for :text
|
67
|
+
@condition_idents << id
|
68
|
+
end
|
69
|
+
|
70
|
+
if index == 0
|
71
|
+
element_by_idents(@condition_idents)
|
72
|
+
else
|
73
|
+
elements_by_idents(@condition_idents)[index]
|
74
|
+
end
|
75
|
+
|
76
|
+
rescue HtmlUnit::ElementNotFoundException
|
77
|
+
nil # for rcov
|
78
|
+
end
|
79
|
+
|
80
|
+
def find_by_id(what)
|
81
|
+
case what
|
82
|
+
when Regexp
|
83
|
+
elements_by_tag_names.find { |elem| elem.getId =~ what }
|
84
|
+
when String
|
85
|
+
obj = @object.getElementById(what)
|
86
|
+
return obj if @tags.include?(obj.getTagName)
|
87
|
+
|
88
|
+
$stderr.puts "warning: multiple elements with identical id? (#{what.inspect})" if $VERBOSE
|
89
|
+
elements_by_tag_names.find { |elem| elem.getId == what }
|
90
|
+
else
|
91
|
+
raise TypeError, "expected String or Regexp, got #{what.inspect}:#{what.class}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def find_by_xpath(what)
|
96
|
+
what = ".#{what}" if what[0].chr == "/"
|
97
|
+
object = @object.getByXPath(what).to_a.first || return
|
98
|
+
|
99
|
+
return unless @idents.any? { |id| id.match?(object) }
|
100
|
+
|
101
|
+
|
102
|
+
object
|
103
|
+
end
|
104
|
+
|
105
|
+
def find_by_label(what)
|
106
|
+
obj = elements_by_tag_names(%w[label]).find { |e| Util.matches?(e.asText, what) }
|
107
|
+
|
108
|
+
return nil unless obj && (ref = obj.getReferencedElement)
|
109
|
+
return ref if @tags.include?(ref.getTagName)
|
110
|
+
|
111
|
+
find_by_id obj.getForAttribute
|
112
|
+
end
|
113
|
+
|
114
|
+
def elements_by_idents(idents = @idents)
|
115
|
+
get_by_idents(:select, idents)
|
116
|
+
end
|
117
|
+
|
118
|
+
def element_by_idents(idents = @idents)
|
119
|
+
get_by_idents(:find, idents)
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def get_by_idents(meth, idents)
|
125
|
+
with_nullpointer_retry do
|
126
|
+
all_elements.send(meth) do |element|
|
127
|
+
next unless @tags.include?(element.getTagName)
|
128
|
+
idents.any? { |id| id.match?(element) }
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def elements_by_tag_names(tags = @tags)
|
134
|
+
with_nullpointer_retry do
|
135
|
+
# HtmlUnit's getHtmlElementsByTagNames won't get elements in the correct
|
136
|
+
# order (making :index fail), so we're looping through all elements instead.
|
137
|
+
all_elements.select { |elem| tags.include?(elem.getTagName) }
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def all_elements
|
142
|
+
unless @object
|
143
|
+
raise %{internal error in #{self.class}: @container=#{@container.inspect} @element_class=#{@element_class.inspect}
|
144
|
+
Please report this failure and the code/HTML that caused it at http://github.com/jarib/celerity/issues}
|
145
|
+
end
|
146
|
+
|
147
|
+
@object.getHtmlElementDescendants
|
148
|
+
end
|
149
|
+
|
150
|
+
# HtmlUnit throws NPEs sometimes when we're locating elements
|
151
|
+
# Retry seems to work fine.
|
152
|
+
def with_nullpointer_retry(max_retries = 3)
|
153
|
+
tries = 0
|
154
|
+
yield
|
155
|
+
rescue java.lang.NullPointerException => e
|
156
|
+
raise e if tries >= max_retries
|
157
|
+
|
158
|
+
tries += 1
|
159
|
+
warn "warning: celerity caught #{e} - retry ##{tries}"
|
160
|
+
retry
|
161
|
+
end
|
162
|
+
|
163
|
+
end # ElementLocator
|
164
|
+
end # Celerity
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Celerity
|
2
|
+
|
3
|
+
#
|
4
|
+
# Input: Button
|
5
|
+
#
|
6
|
+
# Class representing button elements.
|
7
|
+
#
|
8
|
+
# This class covers both <button> and <input type="submit|reset|image|button" /> elements.
|
9
|
+
#
|
10
|
+
|
11
|
+
class Button < InputElement
|
12
|
+
TAGS = [ Identifier.new('button'),
|
13
|
+
Identifier.new('input', :type => %w[submit reset image button]) ]
|
14
|
+
|
15
|
+
# Attribute list is a little weird due to this class covering both <button>
|
16
|
+
# and <input type="submit|reset|image|button" />
|
17
|
+
ATTRIBUTES = ATTRIBUTES | [
|
18
|
+
:accesskey,
|
19
|
+
:disabled,
|
20
|
+
:ismap,
|
21
|
+
:onblur,
|
22
|
+
:onfocus,
|
23
|
+
:src,
|
24
|
+
:tabindex,
|
25
|
+
:type,
|
26
|
+
:usemap,
|
27
|
+
]
|
28
|
+
DEFAULT_HOW = :value
|
29
|
+
|
30
|
+
#
|
31
|
+
# @api private
|
32
|
+
#
|
33
|
+
|
34
|
+
def locate
|
35
|
+
# We want the :value attribute to point to the inner text for <button> elements,
|
36
|
+
# and to the value attribute for <input type="button"> elements.
|
37
|
+
if (val = @conditions[:value])
|
38
|
+
button_ident = Identifier.new('button')
|
39
|
+
button_ident.text = val
|
40
|
+
input_ident = Identifier.new('input', :type => %w[submit reset image button], :value => [val])
|
41
|
+
|
42
|
+
locator = ElementLocator.new(@container, self.class)
|
43
|
+
locator.idents = [button_ident, input_ident]
|
44
|
+
|
45
|
+
conditions = @conditions.dup
|
46
|
+
conditions.delete(:value)
|
47
|
+
@object = locator.find_by_conditions(conditions)
|
48
|
+
else
|
49
|
+
super
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end # Button
|
54
|
+
end # Celerity
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Celerity
|
2
|
+
|
3
|
+
#
|
4
|
+
# For fields that accept file uploads
|
5
|
+
#
|
6
|
+
|
7
|
+
class FileField < InputElement
|
8
|
+
TAGS = [ Identifier.new('input', :type => %w[file]) ]
|
9
|
+
DEFAULT_HOW = :name
|
10
|
+
|
11
|
+
#
|
12
|
+
# Set the file field to the given path
|
13
|
+
#
|
14
|
+
|
15
|
+
def set(path)
|
16
|
+
assert_exists
|
17
|
+
path = path.to_s
|
18
|
+
|
19
|
+
@object.setValueAttribute path
|
20
|
+
|
21
|
+
unless @object.getContentType
|
22
|
+
@object.setContentType(Celerity::Util.content_type_for(path))
|
23
|
+
end
|
24
|
+
|
25
|
+
path
|
26
|
+
end
|
27
|
+
|
28
|
+
end # FileField
|
29
|
+
end # Celerity
|