watir-nokogiri 1.0.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.
Files changed (43) hide show
  1. data/LICENSE +22 -0
  2. data/README.md +65 -0
  3. data/lib/watir-nokogiri.rb +69 -0
  4. data/lib/watir-nokogiri/aliases.rb +6 -0
  5. data/lib/watir-nokogiri/attribute_helper.rb +145 -0
  6. data/lib/watir-nokogiri/cell_container.rb +31 -0
  7. data/lib/watir-nokogiri/container.rb +50 -0
  8. data/lib/watir-nokogiri/document.rb +171 -0
  9. data/lib/watir-nokogiri/element_collection.rb +93 -0
  10. data/lib/watir-nokogiri/elements/button.rb +71 -0
  11. data/lib/watir-nokogiri/elements/checkbox.rb +61 -0
  12. data/lib/watir-nokogiri/elements/dlist.rb +12 -0
  13. data/lib/watir-nokogiri/elements/element.rb +319 -0
  14. data/lib/watir-nokogiri/elements/file_field.rb +45 -0
  15. data/lib/watir-nokogiri/elements/font.rb +11 -0
  16. data/lib/watir-nokogiri/elements/form.rb +17 -0
  17. data/lib/watir-nokogiri/elements/frame.rb +75 -0
  18. data/lib/watir-nokogiri/elements/generated.rb +2662 -0
  19. data/lib/watir-nokogiri/elements/hidden.rb +24 -0
  20. data/lib/watir-nokogiri/elements/image.rb +59 -0
  21. data/lib/watir-nokogiri/elements/input.rb +34 -0
  22. data/lib/watir-nokogiri/elements/link.rb +7 -0
  23. data/lib/watir-nokogiri/elements/option.rb +83 -0
  24. data/lib/watir-nokogiri/elements/radio.rb +44 -0
  25. data/lib/watir-nokogiri/elements/select.rb +126 -0
  26. data/lib/watir-nokogiri/elements/table.rb +44 -0
  27. data/lib/watir-nokogiri/elements/table_cell.rb +36 -0
  28. data/lib/watir-nokogiri/elements/table_row.rb +46 -0
  29. data/lib/watir-nokogiri/elements/table_section.rb +15 -0
  30. data/lib/watir-nokogiri/elements/text_area.rb +22 -0
  31. data/lib/watir-nokogiri/elements/text_field.rb +44 -0
  32. data/lib/watir-nokogiri/exception.rb +20 -0
  33. data/lib/watir-nokogiri/locators/button_locator.rb +54 -0
  34. data/lib/watir-nokogiri/locators/child_cell_locator.rb +24 -0
  35. data/lib/watir-nokogiri/locators/child_row_locator.rb +29 -0
  36. data/lib/watir-nokogiri/locators/element_locator.rb +298 -0
  37. data/lib/watir-nokogiri/locators/text_area_locator.rb +20 -0
  38. data/lib/watir-nokogiri/locators/text_field_locator.rb +71 -0
  39. data/lib/watir-nokogiri/row_container.rb +42 -0
  40. data/lib/watir-nokogiri/user_editable.rb +38 -0
  41. data/lib/watir-nokogiri/version.rb +3 -0
  42. data/lib/watir-nokogiri/xpath_support.rb +22 -0
  43. metadata +102 -0
@@ -0,0 +1,46 @@
1
+ module WatirNokogiri
2
+ class TableRow < HTMLElement
3
+ include CellContainer
4
+
5
+ # @private
6
+ attr_writer :locator_class
7
+
8
+ #
9
+ # Get the n'th cell (<th> or <td>) of this row
10
+ #
11
+ # @return Watir::TableCell
12
+ #
13
+
14
+ def [](idx)
15
+ cell(:index, idx)
16
+ end
17
+
18
+ private
19
+
20
+ def locator_class
21
+ @locator_class || super
22
+ end
23
+ end # TableRow
24
+
25
+ class TableRowCollection < ElementCollection
26
+ attr_writer :locator_class
27
+
28
+ def elements
29
+ # we do this craziness since the xpath used will find direct child rows
30
+ # before any rows inside thead/tbody/tfoot...
31
+ elements = super
32
+
33
+ if locator_class == ChildRowLocator and @parent.kind_of? Table
34
+ # This sorting will not do anything since nokogiri does not know the rowIndex
35
+ # BUG: https://github.com/jkotests/watir-nokogiri/issues/1
36
+ elements = elements.sort_by { |row| row.get_attribute(:rowIndex).to_i }
37
+ end
38
+
39
+ elements
40
+ end
41
+
42
+ def locator_class
43
+ @locator_class || super
44
+ end
45
+ end # TableRowCollection
46
+ end # Watir
@@ -0,0 +1,15 @@
1
+ module WatirNokogiri
2
+ class TableSection < HTMLElement
3
+ include RowContainer
4
+
5
+ #
6
+ # Returns table section row with given index.
7
+ #
8
+ # @param [Fixnum] idx
9
+ #
10
+
11
+ def [](idx)
12
+ row(:index => idx)
13
+ end
14
+ end # TableSection
15
+ end # WatirNokogiri
@@ -0,0 +1,22 @@
1
+ module WatirNokogiri
2
+ class TextArea < HTMLElement
3
+ include UserEditable
4
+
5
+ private
6
+
7
+ def locator_class
8
+ TextAreaLocator
9
+ end
10
+
11
+ end # TextArea
12
+
13
+ class TextAreaCollection < ElementCollection
14
+
15
+ private
16
+
17
+ def locator_class
18
+ TextAreaLocator
19
+ end
20
+
21
+ end # TextAreaCollection
22
+ end # WatirNokogiri
@@ -0,0 +1,44 @@
1
+ # encoding: utf-8
2
+ module WatirNokogiri
3
+ class TextField < Input
4
+ include UserEditable
5
+
6
+ attributes WatirNokogiri::TextArea.typed_attributes
7
+ remove_method :type # we want Input#type here, which was overriden by TextArea's attributes
8
+
9
+ private
10
+
11
+ def locator_class
12
+ TextFieldLocator
13
+ end
14
+
15
+ def selector_string
16
+ selector = @selector.dup
17
+ selector[:type] = '(any text type)'
18
+ selector[:tag_name] = "input or textarea"
19
+ selector.inspect
20
+ end
21
+ end # TextField
22
+
23
+ module Container
24
+ def text_field(*args)
25
+ TextField.new(self, extract_selector(args).merge(:tag_name => "input"))
26
+ end
27
+
28
+ def text_fields(*args)
29
+ TextFieldCollection.new(self, extract_selector(args).merge(:tag_name => "input"))
30
+ end
31
+ end # Container
32
+
33
+ class TextFieldCollection < InputCollection
34
+ private
35
+
36
+ def locator_class
37
+ TextFieldLocator
38
+ end
39
+
40
+ def element_class
41
+ TextField
42
+ end
43
+ end # TextFieldCollection
44
+ end # WatirNokogiri
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ module WatirNokogiri
4
+ module Exception
5
+ class Error < StandardError; end
6
+
7
+ class UnknownObjectException < Error; end
8
+ class ObjectDisabledException < Error; end
9
+ class ObjectReadOnlyException < Error; end
10
+ class NoValueFoundException < Error; end
11
+ class MissingWayOfFindingObjectException < Error; end
12
+ class UnknownCellException < Error; end
13
+ class NoMatchingWindowFoundException < Error; end
14
+ class NoStatusBarException < Error; end
15
+ class NavigationException < Error; end
16
+ class UnknownFrameException < Error; end
17
+ class UnknownRowException < Error; end
18
+
19
+ end # Exception
20
+ end # WatirNokogiri
@@ -0,0 +1,54 @@
1
+ module WatirNokogiri
2
+ class ButtonLocator < ElementLocator
3
+
4
+ def build_nokogiri_selector(selectors)
5
+ return if selectors.values.any? { |e| e.kind_of? Regexp }
6
+
7
+ selectors.delete(:tag_name) || raise("internal error: no tag_name?!")
8
+
9
+ @building = :button
10
+ button_attr_exp = attribute_expression(selectors)
11
+
12
+ @building = :input
13
+ selectors[:type] = Button::VALID_TYPES
14
+ input_attr_exp = attribute_expression(selectors)
15
+
16
+ xpath = ".//button"
17
+ xpath << "[#{button_attr_exp}]" unless button_attr_exp.empty?
18
+ xpath << " | .//input"
19
+ xpath << "[#{input_attr_exp}]"
20
+
21
+ p :build_nokogiri_selector => xpath if $DEBUG
22
+
23
+ [:xpath, xpath]
24
+ end
25
+
26
+ def lhs_for(key)
27
+ if @building == :input && key == :text
28
+ "@value"
29
+ else
30
+ super
31
+ end
32
+ end
33
+
34
+ def equal_pair(key, value)
35
+ if @building == :button && key == :value
36
+ # :value should look for both node text and @value attribute
37
+ text = XpathSupport.escape(value)
38
+ "(text()=#{text} or @value=#{text})"
39
+ else
40
+ super
41
+ end
42
+ end
43
+
44
+ def tag_name_matches?(tag_name, _)
45
+ !!(/^(input|button)$/ === tag_name)
46
+ end
47
+
48
+ def validate_element(element)
49
+ return if element.node_name.downcase == "input" && !Button::VALID_TYPES.include?(element.get_attribute(:type).downcase)
50
+ super
51
+ end
52
+
53
+ end # ButtonLocator
54
+ end # WatirNokogiri
@@ -0,0 +1,24 @@
1
+ module WatirNokogiri
2
+ class ChildCellLocator < ElementLocator
3
+
4
+ private
5
+
6
+ def build_nokogiri_selector(selectors)
7
+ return if selectors.values.any? { |e| e.kind_of? Regexp }
8
+
9
+ expressions = %w[./th ./td]
10
+ attr_expr = attribute_expression(selectors)
11
+
12
+ unless attr_expr.empty?
13
+ expressions.map! { |e| "#{e}[#{attr_expr}]" }
14
+ end
15
+
16
+ xpath = expressions.join(" | ")
17
+
18
+ p :build_nokogiri_selector => xpath if $DEBUG
19
+
20
+ [:xpath, xpath]
21
+ end
22
+
23
+ end # ChildCellLocator
24
+ end # WatirNokogiri
@@ -0,0 +1,29 @@
1
+ module WatirNokogiri
2
+ class ChildRowLocator < ElementLocator
3
+
4
+ private
5
+
6
+ def build_nokogiri_selector(selectors)
7
+ return if selectors.values.any? { |e| e.kind_of? Regexp }
8
+ selectors.delete(:tag_name) || raise("internal error: no tag_name?!")
9
+
10
+ expressions = %w[./tr]
11
+ unless %w[tbody tfoot thead].include?(@nokogiri.node_name.downcase)
12
+ expressions += %w[./tbody/tr ./thead/tr ./tfoot/tr]
13
+ end
14
+
15
+ attr_expr = attribute_expression(selectors)
16
+
17
+ unless attr_expr.empty?
18
+ expressions.map! { |e| "#{e}[#{attr_expr}]" }
19
+ end
20
+
21
+ xpath = expressions.join(" | ")
22
+
23
+ p :build_nokogiri_selector => xpath if $DEBUG
24
+
25
+ [:xpath, xpath]
26
+ end
27
+
28
+ end # ChildRowLocator
29
+ end # WatirNokogiri
@@ -0,0 +1,298 @@
1
+ # encoding: utf-8
2
+ module WatirNokogiri
3
+ class ElementLocator
4
+ include WatirNokogiri::Exception
5
+
6
+ def initialize(nokogiri, selector, valid_attributes)
7
+ @nokogiri = nokogiri
8
+ @selector = selector.dup
9
+ @valid_attributes = valid_attributes
10
+ end
11
+
12
+ def locate()
13
+ idx = @selector.delete(:index)
14
+ if idx
15
+ element = locate_all[idx]
16
+ else
17
+ element = locate_all.first
18
+ end
19
+
20
+ # This actually only applies when finding by xpath - browser.text_field(:xpath, "//input[@type='radio']")
21
+ # We don't need to validate the element if we built the xpath ourselves.
22
+ # It is also used to alter behavior of methods locating more than one type of element
23
+ # (e.g. text_field locates both input and textarea)
24
+ validate_element(element) if element
25
+ end
26
+
27
+ def locate_all()
28
+ selector = normalized_selector
29
+
30
+ if selector.has_key? :index
31
+ raise ArgumentError, "can't locate all elements by :index"
32
+ end
33
+
34
+ how, what = given_css(selector) ||
35
+ given_xpath(selector) ||
36
+ build_nokogiri_selector(selector)
37
+
38
+ case how
39
+ when :css
40
+ @nokogiri.css(what)
41
+ when :xpath
42
+ @nokogiri.xpath(what)
43
+ else
44
+ find_by_regexp_selector(selector, :select)
45
+ end
46
+ end
47
+
48
+ def find_by_regexp_selector(selector, method = :find)
49
+ parent = @nokogiri
50
+ rx_selector = delete_regexps_from(selector)
51
+
52
+ if rx_selector.has_key?(:label) && should_use_label_element?
53
+ label = label_from_text(rx_selector.delete(:label)) || (return [])
54
+ if (id = label.get_attribute('for'))
55
+ selector[:id] = id
56
+ else
57
+ parent = label
58
+ end
59
+ end
60
+
61
+ how, what = build_nokogiri_selector(selector)
62
+
63
+ unless how
64
+ raise Error, "internal error: unable to build WebDriver selector from #{selector.inspect}"
65
+ end
66
+
67
+ elements = parent.xpath(what)
68
+ elements.__send__(method) { |el| matches_selector?(el, rx_selector) }
69
+ end
70
+
71
+ def delete_regexps_from(selector)
72
+ rx_selector = {}
73
+
74
+ selector.dup.each do |how, what|
75
+ next unless what.kind_of?(Regexp)
76
+ rx_selector[how] = what
77
+ selector.delete how
78
+ end
79
+
80
+ rx_selector
81
+ end
82
+
83
+ def assert_valid_as_attribute(attribute)
84
+ unless valid_attribute? attribute or attribute.to_s =~ /^data_.+$/
85
+ raise MissingWayOfFindingObjectException, "invalid attribute: #{attribute.inspect}"
86
+ end
87
+ end
88
+
89
+ def valid_attribute?(attribute)
90
+ @valid_attributes && @valid_attributes.include?(attribute)
91
+ end
92
+
93
+ def should_use_label_element?
94
+ @selector[:tag_name] != "option"
95
+ end
96
+
97
+ def label_from_text(label_exp)
98
+ # TODO: this won't work correctly if @nokogiri is a sub-element
99
+ @nokogiri.search('label').find do |el|
100
+ matches_selector?(el, :text => label_exp)
101
+ end
102
+ end
103
+
104
+ def matches_selector?(element, selector)
105
+ selector.all? do |how, what|
106
+ what === fetch_value(element, how)
107
+ end
108
+ end
109
+
110
+ def fetch_value(element, how)
111
+ case how
112
+ when :text
113
+ element.text
114
+ when :tag_name
115
+ element.node_name.downcase
116
+ when :href
117
+ (href = element.get_attribute('href')) && href.strip
118
+ else
119
+ element.get_attribute(how.to_s.gsub("_", "-"))
120
+ end
121
+ end
122
+
123
+ def normalized_selector
124
+ selector = {}
125
+
126
+ @selector.each do |how, what|
127
+ check_type(how, what)
128
+
129
+ how, what = normalize_selector(how, what)
130
+ selector[how] = what
131
+ end
132
+
133
+ selector
134
+ end
135
+
136
+ def normalize_selector(how, what)
137
+ case how
138
+ when :tag_name, :text, :xpath, :index, :class, :label, :css
139
+ # include :class since the valid attribute is 'class_name'
140
+ # include :for since the valid attribute is 'html_for'
141
+ [how, what]
142
+ when :class_name
143
+ [:class, what]
144
+ when :caption
145
+ [:text, what]
146
+ when :for
147
+ assert_valid_as_attribute :html_for
148
+ [how, what]
149
+ else
150
+ assert_valid_as_attribute how
151
+ [how, what]
152
+ end
153
+ end
154
+
155
+
156
+ VALID_WHATS = [String, Regexp]
157
+
158
+ def check_type(how, what)
159
+ case how
160
+ when :index
161
+ unless what.kind_of?(Fixnum)
162
+ raise TypeError, "expected Fixnum, got #{what.inspect}:#{what.class}"
163
+ end
164
+ else
165
+ unless VALID_WHATS.any? { |t| what.kind_of? t }
166
+ raise TypeError, "expected one of #{VALID_WHATS.inspect}, got #{what.inspect}:#{what.class}"
167
+ end
168
+ end
169
+ end
170
+
171
+ def given_xpath(selector)
172
+ return unless xpath = selector.delete(:xpath)
173
+
174
+ unless selector.empty? || can_be_combined_with_xpath?(selector)
175
+ raise ArgumentError, ":xpath cannot be combined with other selectors (#{selector.inspect})"
176
+ end
177
+
178
+ [:xpath, xpath]
179
+ end
180
+
181
+ def can_be_combined_with_xpath?(selector)
182
+ # ouch - is this worth it?
183
+ keys = selector.keys
184
+ return true if keys == [:tag_name]
185
+
186
+ if selector[:tag_name] == "input"
187
+ return keys == [:tag_name, :type] || keys == [:type, :tag_name]
188
+ end
189
+
190
+ false
191
+ end
192
+
193
+ def given_css(selector)
194
+ return unless css = selector.delete(:css)
195
+
196
+ unless selector.empty? || can_be_combined_with_css?(selector)
197
+ raise ArgumentError, ":css cannot be combined with other selectors (#{selector.inspect})"
198
+ end
199
+
200
+ [:css, xpath]
201
+ end
202
+
203
+ def can_be_combined_with_css?(selector)
204
+ # ouch - is this worth it?
205
+ keys = selector.keys
206
+ return true if keys == [:tag_name]
207
+
208
+ if selector[:tag_name] == "input"
209
+ return keys == [:tag_name, :type] || keys == [:type, :tag_name]
210
+ end
211
+
212
+ false
213
+ end
214
+
215
+ def build_nokogiri_selector(selectors)
216
+ unless selectors.values.any? { |e| e.kind_of? Regexp }
217
+ build_xpath(selectors)
218
+ end
219
+ end
220
+
221
+ def build_xpath(selectors)
222
+ xpath = ".//"
223
+ xpath << (selectors.delete(:tag_name) || '*').to_s
224
+
225
+ idx = selectors.delete :index
226
+
227
+ # the remaining entries should be attributes
228
+ unless selectors.empty?
229
+ xpath << "[" << attribute_expression(selectors) << "]"
230
+ end
231
+
232
+ if idx
233
+ xpath << "[#{idx + 1}]"
234
+ end
235
+
236
+ p :xpath => xpath, :selectors => selectors if $DEBUG
237
+
238
+ [:xpath, xpath]
239
+ end
240
+
241
+ def attribute_expression(selectors)
242
+ selectors.map do |key, val|
243
+ if val.kind_of?(Array)
244
+ "(" + val.map { |v| equal_pair(key, v) }.join(" or ") + ")"
245
+ else
246
+ equal_pair(key, val)
247
+ end
248
+ end.join(" and ")
249
+ end
250
+
251
+ def equal_pair(key, value)
252
+ if key == :class
253
+ klass = XpathSupport.escape " #{value} "
254
+ "contains(concat(' ', @class, ' '), #{klass})"
255
+ elsif key == :label && should_use_label_element?
256
+ # we assume :label means a corresponding label element, not the attribute
257
+ text = "normalize-space()=#{XpathSupport.escape value}"
258
+ "(@id=//label[#{text}]/@for or parent::label[#{text}])"
259
+ else
260
+ "#{lhs_for(key)}=#{XpathSupport.escape value}"
261
+ end
262
+ end
263
+
264
+ def lhs_for(key)
265
+ case key
266
+ when :text, 'text'
267
+ 'normalize-space()'
268
+ when :href
269
+ # TODO: change this behaviour?
270
+ 'normalize-space(@href)'
271
+ when :type
272
+ # type attributes can be upper case - downcase them
273
+ # https://github.com/watir/watir-webdriver/issues/72
274
+ XpathSupport.downcase('@type')
275
+ else
276
+ "@#{key.to_s.gsub("_", "-")}"
277
+ end
278
+ end
279
+
280
+ def validate_element(element)
281
+ tn = @selector[:tag_name]
282
+ element_tag_name = element.node_name.downcase
283
+
284
+ return if tn && !tag_name_matches?(element_tag_name, tn)
285
+
286
+ if element_tag_name == 'input'
287
+ return if @selector[:type] && @selector[:type] != element.get_attribute(:type).downcase
288
+ end
289
+
290
+ element
291
+ end
292
+
293
+ def tag_name_matches?(element_tag_name, tag_name)
294
+ tag_name === element_tag_name
295
+ end
296
+
297
+ end # ElementLocator
298
+ end # WatirNokogiri