watir 6.15.1 → 6.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -2
  3. data/.travis.yml +2 -0
  4. data/CHANGES.md +13 -0
  5. data/Rakefile +6 -0
  6. data/lib/watir.rb +1 -0
  7. data/lib/watir/browser.rb +4 -1
  8. data/lib/watir/element_collection.rb +27 -17
  9. data/lib/watir/elements/element.rb +41 -14
  10. data/lib/watir/elements/iframe.rb +3 -1
  11. data/lib/watir/elements/radio.rb +7 -2
  12. data/lib/watir/elements/select.rb +1 -0
  13. data/lib/watir/locators.rb +21 -21
  14. data/lib/watir/locators/button/matcher.rb +40 -0
  15. data/lib/watir/locators/cell/selector_builder.rb +3 -0
  16. data/lib/watir/locators/element/locator.rb +29 -172
  17. data/lib/watir/locators/element/matcher.rb +127 -0
  18. data/lib/watir/locators/element/selector_builder.rb +69 -23
  19. data/lib/watir/locators/element/selector_builder/xpath.rb +3 -10
  20. data/lib/watir/locators/row/selector_builder.rb +5 -5
  21. data/lib/watir/locators/text_area/selector_builder.rb +0 -14
  22. data/lib/watir/locators/text_area/selector_builder/xpath.rb +2 -2
  23. data/lib/watir/locators/text_field/matcher.rb +38 -0
  24. data/lib/watir/radio_set.rb +28 -31
  25. data/lib/watir/scroll.rb +69 -0
  26. data/lib/watir/version.rb +1 -1
  27. data/spec/locator_spec_helper.rb +58 -14
  28. data/spec/unit/element_locator_spec.rb +46 -591
  29. data/spec/unit/match_elements/button_spec.rb +80 -0
  30. data/spec/unit/match_elements/element_spec.rb +368 -0
  31. data/spec/unit/match_elements/text_field_spec.rb +79 -0
  32. data/spec/unit/selector_builder/anchor_spec.rb +51 -0
  33. data/spec/unit/selector_builder/button_spec.rb +206 -0
  34. data/spec/unit/selector_builder/cell_spec.rb +63 -0
  35. data/spec/unit/selector_builder/element_spec.rb +744 -0
  36. data/spec/unit/selector_builder/row_spec.rb +111 -0
  37. data/spec/unit/selector_builder/text_field_spec.rb +189 -0
  38. data/spec/unit/selector_builder/textarea_spec.rb +25 -0
  39. data/spec/watirspec/browser_spec.rb +7 -8
  40. data/spec/watirspec/element_hidden_spec.rb +1 -2
  41. data/spec/watirspec/elements/element_spec.rb +52 -16
  42. data/spec/watirspec/elements/iframe_spec.rb +1 -1
  43. data/spec/watirspec/elements/select_list_spec.rb +1 -1
  44. data/spec/watirspec/html/obscured.html +3 -1
  45. data/spec/watirspec/html/scroll.html +32 -0
  46. data/spec/watirspec/relaxed_locate_spec.rb +6 -1
  47. data/spec/watirspec/scroll_spec.rb +106 -0
  48. data/spec/watirspec/support/rspec_matchers.rb +2 -0
  49. data/spec/watirspec/wait_spec.rb +1 -1
  50. data/watir.gemspec +2 -4
  51. metadata +36 -33
  52. data/lib/watir/locators/button/locator.rb +0 -32
  53. data/lib/watir/locators/button/validator.rb +0 -17
  54. data/lib/watir/locators/cell/locator.rb +0 -13
  55. data/lib/watir/locators/element/validator.rb +0 -11
  56. data/lib/watir/locators/row/locator.rb +0 -13
  57. data/lib/watir/locators/text_field/locator.rb +0 -31
  58. data/lib/watir/locators/text_field/validator.rb +0 -13
  59. data/spec/unit/anchor_locator_spec.rb +0 -68
  60. data/spec/watirspec/selector_builder/button_spec.rb +0 -250
  61. data/spec/watirspec/selector_builder/cell_spec.rb +0 -92
  62. data/spec/watirspec/selector_builder/element_spec.rb +0 -628
  63. data/spec/watirspec/selector_builder/row_spec.rb +0 -148
  64. data/spec/watirspec/selector_builder/text_spec.rb +0 -199
@@ -0,0 +1,51 @@
1
+ require_relative '../unit_helper'
2
+
3
+ describe Watir::Locators::Anchor::SelectorBuilder do
4
+ include LocatorSpecHelper
5
+
6
+ let(:selector_builder) { described_class.new(attributes, query_scope) }
7
+
8
+ describe '#build' do
9
+ it 'without only tag name' do
10
+ selector = {tag_name: 'a'}
11
+ built = {xpath: ".//*[local-name()='a']"}
12
+
13
+ expect(selector_builder.build(selector)).to eq built
14
+ end
15
+
16
+ it 'converts visible_text String to link_text' do
17
+ selector = {tag_name: 'a', visible_text: 'Foo'}
18
+ built = {link_text: 'Foo'}
19
+
20
+ expect(selector_builder.build(selector)).to eq built
21
+ end
22
+
23
+ it 'converts visible_text Regexp to partial_link_text' do
24
+ selector = {tag_name: 'a', visible_text: /Foo/}
25
+ built = {partial_link_text: 'Foo'}
26
+
27
+ expect(selector_builder.build(selector)).to eq built
28
+ end
29
+
30
+ it 'does not convert visible_text with casefold Regexp to partial_link_text' do
31
+ selector = {tag_name: 'a', visible_text: /partial text/i}
32
+ built = {xpath: ".//*[local-name()='a']", visible_text: /partial text/i}
33
+
34
+ expect(selector_builder.build(selector)).to eq built
35
+ end
36
+
37
+ it 'does not convert :visible_text with String and other locators' do
38
+ selector = {tag_name: 'a', visible_text: 'Foo', id: 'Foo'}
39
+ built = {xpath: ".//*[local-name()='a'][@id='Foo']", visible_text: 'Foo'}
40
+
41
+ expect(selector_builder.build(selector)).to eq built
42
+ end
43
+
44
+ it 'does not convert :visible_text with Regexp and other locators' do
45
+ selector = {tag_name: 'a', visible_text: /Foo/, id: 'Foo'}
46
+ built = {xpath: ".//*[local-name()='a'][@id='Foo']", visible_text: /Foo/}
47
+
48
+ expect(selector_builder.build(selector)).to eq built
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,206 @@
1
+ require_relative '../unit_helper'
2
+
3
+ describe Watir::Locators::Button::SelectorBuilder do
4
+ include LocatorSpecHelper
5
+
6
+ let(:selector_builder) { described_class.new(attributes, query_scope) }
7
+ let(:uppercase) { 'ABCDEFGHIJKLMNOPQRSTUVWXYZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞŸŽŠŒ' }
8
+ let(:lowercase) { 'abcdefghijklmnopqrstuvwxyzàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿžšœ' }
9
+ let(:default_types) do
10
+ "translate(@type,'#{uppercase}','#{lowercase}')='button' or" \
11
+ " translate(@type,'#{uppercase}','#{lowercase}')='reset' or"\
12
+ " translate(@type,'#{uppercase}','#{lowercase}')='submit' or"\
13
+ " translate(@type,'#{uppercase}','#{lowercase}')='image'"
14
+ end
15
+
16
+ describe '#build' do
17
+ it 'without any arguments' do
18
+ selector = {}
19
+ built = {xpath: ".//*[(local-name()='button') or (local-name()='input' and (#{default_types}))]"}
20
+ expect(selector_builder.build(selector)).to eq built
21
+ end
22
+
23
+ context 'with type' do
24
+ it 'false only locates with button without a type' do
25
+ selector = {type: false}
26
+ built = {xpath: ".//*[(local-name()='button' and not(@type))]"}
27
+ expect(selector_builder.build(selector)).to eq built
28
+ end
29
+
30
+ it 'true locates button or input with a type' do
31
+ selector = {type: true}
32
+ built = {xpath: ".//*[(local-name()='button' and @type) or " \
33
+ "(local-name()='input' and (#{default_types}))]"}
34
+ expect(selector_builder.build(selector)).to eq built
35
+ end
36
+
37
+ it 'locates input or button element with specified type' do
38
+ selector = {type: 'reset'}
39
+ built = {xpath: ".//*[(local-name()='button' and " \
40
+ "translate(@type,'#{uppercase}','#{lowercase}')='reset') or " \
41
+ "(local-name()='input' and (translate(@type,'#{uppercase}','#{lowercase}')='reset'))]"}
42
+ expect(selector_builder.build(selector)).to eq built
43
+ end
44
+
45
+ it 'raises exception when a non-button type input is specified' do
46
+ selector = {type: 'checkbox'}
47
+ msg = 'Button Elements can not be located by input type: checkbox'
48
+ expect { selector_builder.build(selector) }
49
+ .to raise_exception Watir::Exception::LocatorException, msg
50
+ end
51
+ end
52
+
53
+ context 'with xpath or css' do
54
+ it 'returns tag name and type to the locator' do
55
+ selector = {xpath: '#disabled_button', tag_name: 'input', type: 'submit'}
56
+ built = {xpath: '#disabled_button', tag_name: 'input', type: 'submit'}
57
+ expect(selector_builder.build(selector)).to eq built
58
+ end
59
+ end
60
+
61
+ context 'with text' do
62
+ it 'locates value of input element with String' do
63
+ selector = {text: 'Button'}
64
+ built = {xpath: ".//*[(local-name()='button' and normalize-space()='Button') or " \
65
+ "(local-name()='input' and (#{default_types}) and @value='Button')]"}
66
+ expect(selector_builder.build(selector)).to eq built
67
+ end
68
+
69
+ it 'locates text of button element with String' do
70
+ selector = {text: 'Button 2'}
71
+ built = {xpath: ".//*[(local-name()='button' and normalize-space()='Button 2') or " \
72
+ "(local-name()='input' and (#{default_types}) and @value='Button 2')]"}
73
+ expect(selector_builder.build(selector)).to eq built
74
+ end
75
+
76
+ it 'locates value of input element with simple Regexp' do
77
+ selector = {text: /Button/}
78
+ built = {xpath: ".//*[(local-name()='button' and contains(text(), 'Button')) or " \
79
+ "(local-name()='input' and (#{default_types}) and contains(@value, 'Button'))]"}
80
+ expect(selector_builder.build(selector)).to eq built
81
+ end
82
+
83
+ it 'locates text of button element with simple Regexp' do
84
+ selector = {text: /Button 2/}
85
+ built = {xpath: ".//*[(local-name()='button' and contains(text(), 'Button 2')) or " \
86
+ "(local-name()='input' and (#{default_types}) and contains(@value, 'Button 2'))]"}
87
+ expect(selector_builder.build(selector)).to eq built
88
+ end
89
+
90
+ it 'Simple Regexp for text' do
91
+ selector = {text: /n 2/}
92
+ built = {xpath: ".//*[(local-name()='button' and contains(text(), 'n 2')) or " \
93
+ "(local-name()='input' and (#{default_types}) and contains(@value, 'n 2'))]"}
94
+ expect(selector_builder.build(selector)).to eq built
95
+ end
96
+
97
+ it 'Simple Regexp for value' do
98
+ selector = {text: /Prev/}
99
+ built = {xpath: ".//*[(local-name()='button' and contains(text(), 'Prev')) or " \
100
+ "(local-name()='input' and (#{default_types}) and contains(@value, 'Prev'))]"}
101
+ expect(selector_builder.build(selector)).to eq built
102
+ end
103
+
104
+ it 'returns complex Regexp to the locator' do
105
+ selector = {text: /^foo$/}
106
+ built = {xpath: ".//*[(local-name()='button' and contains(text(), 'foo')) or " \
107
+ "(local-name()='input' and (#{default_types}) and contains(@value, 'foo'))]", text: /^foo$/}
108
+ expect(selector_builder.build(selector)).to eq built
109
+ end
110
+ end
111
+
112
+ context 'with value' do
113
+ it 'input element value with String' do
114
+ selector = {value: 'Preview'}
115
+ built = {xpath: ".//*[(local-name()='button') or (local-name()='input' and (#{default_types}))]" \
116
+ "[normalize-space()='Preview' or @value='Preview']"}
117
+ expect(selector_builder.build(selector)).to eq built
118
+ end
119
+
120
+ it 'button element value with String' do
121
+ selector = {value: 'button_2'}
122
+ built = {xpath: ".//*[(local-name()='button') or (local-name()='input' and (#{default_types}))]" \
123
+ "[normalize-space()='button_2' or @value='button_2']"}
124
+ expect(selector_builder.build(selector)).to eq built
125
+ end
126
+
127
+ it 'input element value with simple Regexp' do
128
+ selector = {value: /Prev/}
129
+ built = {xpath: ".//*[(local-name()='button') or (local-name()='input' and (#{default_types}))]" \
130
+ "[contains(text(), 'Prev') or contains(@value, 'Prev')]"}
131
+ expect(selector_builder.build(selector)).to eq built
132
+ end
133
+
134
+ it 'button element value with simple Regexp' do
135
+ selector = {value: /on_2/}
136
+ built = {xpath: ".//*[(local-name()='button') or (local-name()='input' and (#{default_types}))]" \
137
+ "[contains(text(), 'on_2') or contains(@value, 'on_2')]"}
138
+ expect(selector_builder.build(selector)).to eq built
139
+ end
140
+
141
+ it 'button element text with String' do
142
+ selector = {value: 'Button 2'}
143
+ built = {xpath: ".//*[(local-name()='button') or (local-name()='input' and (#{default_types}))]" \
144
+ "[normalize-space()='Button 2' or @value='Button 2']"}
145
+ expect(selector_builder.build(selector)).to eq built
146
+ end
147
+
148
+ it 'button element text with simple Regexp' do
149
+ selector = {value: /ton 2/}
150
+ built = {xpath: ".//*[(local-name()='button') or (local-name()='input' and (#{default_types}))]" \
151
+ "[contains(text(), 'ton 2') or contains(@value, 'ton 2')]"}
152
+ expect(selector_builder.build(selector)).to eq built
153
+ end
154
+
155
+ it 'returns complex Regexp to the locator' do
156
+ selector = {value: /^foo$/}
157
+ built = {xpath: ".//*[(local-name()='button') or (local-name()='input' and (#{default_types}))]" \
158
+ "[contains(text(), 'foo') or contains(@value, 'foo')]", value: /^foo$/}
159
+ expect(selector_builder.build(selector)).to eq built
160
+ end
161
+ end
162
+
163
+ context 'with index' do
164
+ it 'positive' do
165
+ selector = {index: 3}
166
+ built = {xpath: "(.//*[(local-name()='button') or (local-name()='input' and (#{default_types}))])[4]"}
167
+ expect(selector_builder.build(selector)).to eq built
168
+ end
169
+
170
+ it 'negative' do
171
+ selector = {index: -4}
172
+ built = {xpath: "(.//*[(local-name()='button') or " \
173
+ "(local-name()='input' and (#{default_types}))])[last()-3]"}
174
+ expect(selector_builder.build(selector)).to eq built
175
+ end
176
+
177
+ it 'last' do
178
+ selector = {index: -1}
179
+ built = {xpath: "(.//*[(local-name()='button') or " \
180
+ "(local-name()='input' and (#{default_types}))])[last()]"}
181
+ expect(selector_builder.build(selector)).to eq built
182
+ end
183
+
184
+ it 'does not return index if it is zero' do
185
+ selector = {index: 0}
186
+ built = {xpath: ".//*[(local-name()='button') or (local-name()='input' and (#{default_types}))]"}
187
+ expect(selector_builder.build(selector)).to eq built
188
+ end
189
+
190
+ it 'raises exception when index is not an Integer', skip_after: true do
191
+ selector = {index: 'foo'}
192
+ msg = /expected one of \[(Integer|Fixnum)\], got "foo":String/
193
+ expect { selector_builder.build(selector) }.to raise_exception TypeError, msg
194
+ end
195
+ end
196
+
197
+ context 'with multiple locators' do
198
+ it 'locates using class and attributes' do
199
+ selector = {class: 'image', name: 'new_user_image', src: true}
200
+ built = {xpath: ".//*[(local-name()='button') or (local-name()='input' and (#{default_types}))]" \
201
+ "[contains(concat(' ', @class, ' '), ' image ')][@name='new_user_image' and @src]"}
202
+ expect(selector_builder.build(selector)).to eq built
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,63 @@
1
+ require_relative '../unit_helper'
2
+
3
+ describe Watir::Locators::Cell::SelectorBuilder do
4
+ include LocatorSpecHelper
5
+
6
+ let(:selector_builder) { described_class.new(attributes, query_scope) }
7
+
8
+ describe '#build' do
9
+ it 'without any arguments' do
10
+ selector = {}
11
+ built = {xpath: "./*[local-name()='th' or local-name()='td']"}
12
+
13
+ expect(selector_builder.build(selector)).to eq built
14
+ end
15
+
16
+ context 'with index' do
17
+ it 'positive' do
18
+ selector = {index: 3}
19
+ built = {xpath: "(./*[local-name()='th' or local-name()='td'])[4]"}
20
+
21
+ expect(selector_builder.build(selector)).to eq built
22
+ end
23
+
24
+ it 'negative' do
25
+ selector = {index: -3}
26
+ built = {xpath: "(./*[local-name()='th' or local-name()='td'])[last()-2]"}
27
+
28
+ expect(selector_builder.build(selector)).to eq built
29
+ end
30
+
31
+ it 'last' do
32
+ selector = {index: -1}
33
+ built = {xpath: "(./*[local-name()='th' or local-name()='td'])[last()]"}
34
+
35
+ expect(selector_builder.build(selector)).to eq built
36
+ end
37
+
38
+ it 'does not return index if it is zero' do
39
+ selector = {index: 0}
40
+ built = {xpath: "./*[local-name()='th' or local-name()='td']"}
41
+
42
+ expect(selector_builder.build(selector)).to eq built
43
+ end
44
+
45
+ it 'raises exception when index is not an Integer' do
46
+ selector = {index: 'foo'}
47
+ msg = /expected one of \[(Integer|Fixnum)\], got "foo":String/
48
+
49
+ expect { selector_builder.build(selector) }.to raise_exception TypeError, msg
50
+ end
51
+ end
52
+
53
+ context 'with multiple locators' do
54
+ it 'attribute and text' do
55
+ selector = {headers: /before_tax/, text: '5 934'}
56
+ built = {xpath: "./*[local-name()='th' or local-name()='td']" \
57
+ "[normalize-space()='5 934'][contains(@headers, 'before_tax')]"}
58
+
59
+ expect(selector_builder.build(selector)).to eq built
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,744 @@
1
+ require_relative '../unit_helper'
2
+
3
+ describe Watir::Locators::Element::SelectorBuilder do
4
+ include LocatorSpecHelper
5
+
6
+ let(:selector_builder) { described_class.new(attributes, query_scope) }
7
+
8
+ describe '#build' do
9
+ it 'without any arguments' do
10
+ selector = {}
11
+ built = {xpath: './/*'}
12
+
13
+ expect(selector_builder.build(selector)).to eq built
14
+ end
15
+
16
+ context 'with xpath or css' do
17
+ it 'locates with xpath only' do
18
+ selector = {xpath: './/div'}
19
+ built = selector.dup
20
+
21
+ expect(selector_builder.build(selector)).to eq built
22
+ end
23
+
24
+ it 'locates with css only' do
25
+ selector = {css: 'div'}
26
+ built = selector.dup
27
+
28
+ expect(selector_builder.build(selector)).to eq built
29
+ end
30
+
31
+ it 'locates when attributes combined with xpath' do
32
+ selector = {xpath: './/div', random: 'foo'}
33
+ built = selector.dup
34
+
35
+ expect(selector_builder.build(selector)).to eq built
36
+ end
37
+
38
+ it 'locates when attributes combined with css' do
39
+ selector = {css: 'div', random: 'foo'}
40
+ built = selector.dup
41
+
42
+ expect(selector_builder.build(selector)).to eq built
43
+ end
44
+
45
+ it 'raises exception when using xpath & css' do
46
+ selector = {xpath: './/*', css: 'div'}
47
+ msg = 'Can not locate element with [:xpath, :css]'
48
+
49
+ expect { selector_builder.build(selector) }.to raise_exception Watir::Exception::LocatorException, msg
50
+ end
51
+
52
+ it 'raises exception when not a String' do
53
+ selector = {xpath: 7}
54
+ msg = /expected one of \[String\], got 7:(Fixnum|Integer)/
55
+
56
+ expect { selector_builder.build(selector) }.to raise_exception TypeError, msg
57
+ end
58
+ end
59
+
60
+ context 'with tag_name' do
61
+ it 'with String equals' do
62
+ selector = {tag_name: 'div'}
63
+ built = {xpath: ".//*[local-name()='div']"}
64
+
65
+ expect(selector_builder.build(selector)).to eq built
66
+ end
67
+
68
+ it 'with simple Regexp contains' do
69
+ selector = {tag_name: /div/}
70
+ built = {xpath: ".//*[contains(local-name(), 'div')]"}
71
+
72
+ expect(selector_builder.build(selector)).to eq built
73
+ end
74
+
75
+ it 'with Symbol' do
76
+ selector = {tag_name: :div}
77
+ built = {xpath: ".//*[local-name()='div']"}
78
+
79
+ expect(selector_builder.build(selector)).to eq built
80
+ end
81
+
82
+ it 'raises exception when not a String or Regexp' do
83
+ selector = {tag_name: 7}
84
+ msg = /expected one of \[String, Regexp, Symbol\], got 7:(Fixnum|Integer)/
85
+
86
+ expect { selector_builder.build(selector) }.to raise_exception TypeError, msg
87
+ end
88
+ end
89
+
90
+ context 'with class names' do
91
+ it 'class_name is converted to class' do
92
+ selector = {class_name: 'user'}
93
+ built = {xpath: ".//*[contains(concat(' ', @class, ' '), ' user ')]"}
94
+
95
+ expect(selector_builder.build(selector)).to eq built
96
+ end
97
+
98
+ # TODO: This functionality is deprecated with "class_array"
99
+ it 'values with spaces' do
100
+ selector = {class_name: 'multiple classes here'}
101
+ built = {xpath: ".//*[contains(concat(' ', @class, ' '), ' multiple classes here ')]"}
102
+
103
+ expect(selector_builder.build(selector)).to eq built
104
+ end
105
+
106
+ it 'single String concatenates' do
107
+ selector = {class: 'user'}
108
+ built = {xpath: ".//*[contains(concat(' ', @class, ' '), ' user ')]"}
109
+
110
+ expect(selector_builder.build(selector)).to eq built
111
+ end
112
+
113
+ it 'Array of String concatenates with and' do
114
+ selector = {class: %w[multiple here]}
115
+ built = {xpath: ".//*[contains(concat(' ', @class, ' '), ' multiple ') and " \
116
+ "contains(concat(' ', @class, ' '), ' here ')]"}
117
+
118
+ expect(selector_builder.build(selector)).to eq built
119
+ end
120
+
121
+ it 'merges values when class and class_name are both used' do
122
+ selector = {class: 'foo', class_name: 'bar'}
123
+ built = {xpath: ".//*[contains(concat(' ', @class, ' '), ' foo ') and " \
124
+ "contains(concat(' ', @class, ' '), ' bar ')]"}
125
+
126
+ expect(selector_builder.build(selector)).to eq built
127
+ end
128
+
129
+ it 'simple Regexp contains' do
130
+ selector = {class_name: /use/}
131
+ built = {xpath: ".//*[contains(@class, 'use')]"}
132
+
133
+ expect(selector_builder.build(selector)).to eq built
134
+ end
135
+
136
+ it 'Array of Regexp contains with and' do
137
+ selector = {class: [/mult/, /her/]}
138
+ built = {xpath: ".//*[contains(@class, 'mult') and contains(@class, 'her')]"}
139
+
140
+ expect(selector_builder.build(selector)).to eq built
141
+ end
142
+
143
+ it 'single negated String concatenates with not' do
144
+ selector = {class: '!multiple'}
145
+ built = {xpath: ".//*[not(contains(concat(' ', @class, ' '), ' multiple '))]"}
146
+
147
+ expect(selector_builder.build(selector)).to eq built
148
+ end
149
+
150
+ it 'single Boolean true provides the at' do
151
+ selector = {class: true}
152
+ built = {xpath: './/*[@class]'}
153
+
154
+ expect(selector_builder.build(selector)).to eq built
155
+ end
156
+
157
+ it 'single Boolean false provides the not atat' do
158
+ selector = {class: false}
159
+ built = {xpath: './/*[not(@class)]'}
160
+
161
+ expect(selector_builder.build(selector)).to eq built
162
+ end
163
+
164
+ it 'Array of mixed String, Regexp and Boolean contains and concatenates with and and not' do
165
+ selector = {class: [/mult/, 'classes', '!here']}
166
+ built = {xpath: ".//*[contains(@class, 'mult') and contains(concat(' ', @class, ' '), ' classes ') " \
167
+ "and not(contains(concat(' ', @class, ' '), ' here '))]"}
168
+
169
+ expect(selector_builder.build(selector)).to eq built
170
+ end
171
+
172
+ it 'empty string finds elements without class' do
173
+ selector = {class_name: ''}
174
+ built = {xpath: './/*[not(@class)]'}
175
+
176
+ expect(selector_builder.build(selector)).to eq built
177
+ end
178
+
179
+ it 'empty Array finds elements without class' do
180
+ selector = {class_name: []}
181
+ built = {xpath: './/*[not(@class)]'}
182
+
183
+ expect(selector_builder.build(selector)).to eq built
184
+ end
185
+
186
+ it 'raises exception when not a String or Regexp or Array' do
187
+ selector = {class: 7}
188
+ msg = /expected one of \[String, Regexp, TrueClass, FalseClass\], got 7:(Fixnum|Integer)/
189
+
190
+ expect { selector_builder.build(selector) }.to raise_exception TypeError, msg
191
+ end
192
+
193
+ it 'raises exception when Array values are not a String or Regexp' do
194
+ selector = {class: [7]}
195
+ msg = /expected one of \[String, Regexp, TrueClass, FalseClass\], got 7:(Fixnum|Integer)/
196
+
197
+ expect { selector_builder.build(selector) }.to raise_exception TypeError, msg
198
+ end
199
+ end
200
+
201
+ context 'with attributes as predicates' do
202
+ it 'with href attribute' do
203
+ selector = {href: 'watirspec.css'}
204
+ built = {xpath: ".//*[normalize-space(@href)='watirspec.css']"}
205
+
206
+ expect(selector_builder.build(selector)).to eq built
207
+ end
208
+
209
+ it 'with String attribute key' do
210
+ selector = {'id' => 'user_new'}
211
+ built = {xpath: ".//*[@id='user_new']"}
212
+
213
+ expect(selector_builder.build(selector)).to eq built
214
+ end
215
+
216
+ it 'with String equals' do
217
+ selector = {id: 'user_new'}
218
+ built = {xpath: ".//*[@id='user_new']"}
219
+
220
+ expect(selector_builder.build(selector)).to eq built
221
+ end
222
+
223
+ it 'with TrueClass no equals' do
224
+ selector = {tag_name: 'input', id: true}
225
+ built = {xpath: ".//*[local-name()='input'][@id]"}
226
+
227
+ expect(selector_builder.build(selector)).to eq built
228
+ end
229
+
230
+ it 'with FalseClass not with no equals' do
231
+ selector = {tag_name: 'input', name: false}
232
+ built = {xpath: ".//*[local-name()='input'][not(@name)]"}
233
+
234
+ expect(selector_builder.build(selector)).to eq built
235
+ end
236
+
237
+ it 'with multiple attributes: no equals and not with no equals and equals' do
238
+ selector = {readonly: true, foo: false, id: 'good_luck'}
239
+ built = {xpath: ".//*[@readonly and not(@foo) and @id='good_luck']"}
240
+
241
+ expect(selector_builder.build(selector)).to eq built
242
+ end
243
+
244
+ it 'raises exception when attribute value is not a Boolean, String or Regexp' do
245
+ selector = {foo: 7}
246
+ msg = /expected one of \[String, Regexp, TrueClass, FalseClass\], got 7:(Fixnum|Integer)/
247
+
248
+ expect { selector_builder.build(selector) }.to raise_exception TypeError, msg
249
+ end
250
+
251
+ it 'raises exception when attribute key is not a String or Regexp' do
252
+ selector = {7 => 'foo'}
253
+ msg = /Unable to build XPath using 7:(Fixnum|Integer)/
254
+
255
+ expect { selector_builder.build(selector) }.to raise_exception Watir::Exception::LocatorException, msg
256
+ end
257
+ end
258
+
259
+ context 'with attributes as partials' do
260
+ it 'with Regexp' do
261
+ selector = {name: /user/}
262
+ built = {xpath: ".//*[contains(@name, 'user')]"}
263
+
264
+ expect(selector_builder.build(selector)).to eq built
265
+ end
266
+
267
+ it 'with multiple Regexp attributes separated by and' do
268
+ selector = {readonly: /read/, id: /good/}
269
+ built = {xpath: ".//*[contains(@readonly, 'read') and contains(@id, 'good')]"}
270
+
271
+ expect(selector_builder.build(selector)).to eq built
272
+ end
273
+ end
274
+
275
+ context 'with text' do
276
+ it 'String uses normalize space equals' do
277
+ selector = {text: 'Add user'}
278
+ built = {xpath: ".//*[normalize-space()='Add user']"}
279
+
280
+ expect(selector_builder.build(selector)).to eq built
281
+ end
282
+
283
+ # Deprecated with :caption
284
+ it 'with caption attribute' do
285
+ selector = {caption: 'Add user'}
286
+ built = {xpath: ".//*[normalize-space()='Add user']"}
287
+
288
+ expect(selector_builder.build(selector)).to eq built
289
+ end
290
+
291
+ it 'raises exception when text is not a String or Regexp' do
292
+ selector = {text: 7}
293
+ msg = /expected one of \[String, Regexp\], got 7:(Fixnum|Integer)/
294
+
295
+ expect { selector_builder.build(selector) }.to raise_exception TypeError, msg
296
+ end
297
+ end
298
+
299
+ context 'with index' do
300
+ it 'positive' do
301
+ selector = {tag_name: 'div', index: 7}
302
+ built = {xpath: "(.//*[local-name()='div'])[8]"}
303
+
304
+ expect(selector_builder.build(selector)).to eq built
305
+ end
306
+
307
+ it 'negative' do
308
+ selector = {tag_name: 'div', index: -7}
309
+ built = {xpath: "(.//*[local-name()='div'])[last()-6]"}
310
+
311
+ expect(selector_builder.build(selector)).to eq built
312
+ end
313
+
314
+ it 'last' do
315
+ selector = {tag_name: 'div', index: -1}
316
+ built = {xpath: "(.//*[local-name()='div'])[last()]"}
317
+
318
+ expect(selector_builder.build(selector)).to eq built
319
+ end
320
+
321
+ it 'does not return index if it is zero' do
322
+ selector = {tag_name: 'div', index: 0}
323
+ built = {xpath: ".//*[local-name()='div']"}
324
+
325
+ expect(selector_builder.build(selector)).to eq built
326
+ end
327
+
328
+ it 'raises exception when index is not an Integer' do
329
+ selector = {index: 'foo'}
330
+ msg = /expected one of \[(Integer|Fixnum)\], got "foo":String/
331
+
332
+ expect { selector_builder.build(selector) }.to raise_exception TypeError, msg
333
+ end
334
+ end
335
+
336
+ context 'with labels' do
337
+ it 'locates the element associated with the label element located by the text of the provided label key' do
338
+ selector = {label: 'Cars'}
339
+ built = {xpath: ".//*[@id=//label[normalize-space()='Cars']/@for "\
340
+ "or parent::label[normalize-space()='Cars']]"}
341
+
342
+ expect(selector_builder.build(selector)).to eq built
343
+ end
344
+
345
+ it 'returns a label_element if complex' do
346
+ selector = {label: /Ca|rs/}
347
+ built = {xpath: './/*', label_element: /Ca|rs/}
348
+
349
+ expect(selector_builder.build(selector)).to eq built
350
+ end
351
+
352
+ it 'returns a visible_label_element if complex' do
353
+ selector = {visible_label: /Ca|rs/}
354
+ built = {xpath: './/*', visible_label_element: /Ca|rs/}
355
+
356
+ expect(selector_builder.build(selector)).to eq built
357
+ end
358
+
359
+ it 'does not use the label element when label is a valid attribute' do
360
+ @attributes ||= Watir::Option.attribute_list
361
+
362
+ selector = {tag_name: 'option', label: 'Germany'}
363
+ built = {xpath: ".//*[local-name()='option'][@label='Germany']"}
364
+
365
+ expect(selector_builder.build(selector)).to eq built
366
+ end
367
+ end
368
+
369
+ context 'with adjacent locators' do
370
+ it 'raises exception when not a Symbol' do
371
+ selector = {adjacent: 'foo', index: 0}
372
+ msg = 'expected one of [Symbol], got "foo":String'
373
+
374
+ expect { selector_builder.build(selector) }.to raise_exception TypeError, msg
375
+ end
376
+
377
+ it 'raises exception when not a valid value' do
378
+ selector = {adjacent: :foo, index: 0}
379
+ msg = 'Unable to process adjacent locator with foo'
380
+
381
+ expect { selector_builder.build(selector) }.to raise_exception Watir::Exception::LocatorException, msg
382
+ end
383
+
384
+ describe '#parent' do
385
+ it 'with no other arguments' do
386
+ selector = {adjacent: :ancestor, index: 0}
387
+ built = {xpath: './ancestor::*[1]'}
388
+
389
+ expect(selector_builder.build(selector)).to eq built
390
+ end
391
+
392
+ it 'with index' do
393
+ selector = {adjacent: :ancestor, index: 2}
394
+ built = {xpath: './ancestor::*[3]'}
395
+
396
+ expect(selector_builder.build(selector)).to eq built
397
+ end
398
+
399
+ it 'with multiple locators' do
400
+ selector = {adjacent: :ancestor, id: true, tag_name: 'div', class: 'ancestor', index: 1}
401
+ built = {xpath: "./ancestor::*[local-name()='div']"\
402
+ "[contains(concat(' ', @class, ' '), ' ancestor ')][@id][2]"}
403
+
404
+ expect(selector_builder.build(selector)).to eq built
405
+ end
406
+
407
+ it 'raises an exception if text locator is used' do
408
+ selector = {adjacent: :ancestor, index: 0, text: 'Foo'}
409
+ msg = 'Can not find parent element with text locator'
410
+ expect { selector_builder.build(selector) }
411
+ .to raise_exception Watir::Exception::LocatorException, msg
412
+ end
413
+ end
414
+
415
+ describe '#following_sibling' do
416
+ it 'with no other arguments' do
417
+ selector = {adjacent: :following, index: 0}
418
+ built = {xpath: './following-sibling::*[1]'}
419
+
420
+ expect(selector_builder.build(selector)).to eq built
421
+ end
422
+
423
+ it 'with index' do
424
+ selector = {adjacent: :following, index: 2}
425
+ built = {xpath: './following-sibling::*[3]'}
426
+
427
+ expect(selector_builder.build(selector)).to eq built
428
+ end
429
+
430
+ it 'with multiple locators' do
431
+ selector = {adjacent: :following, tag_name: 'div', class: 'b', index: 0, id: true}
432
+ built = {xpath: "./following-sibling::*[local-name()='div']"\
433
+ "[contains(concat(' ', @class, ' '), ' b ')][@id][1]"}
434
+
435
+ expect(selector_builder.build(selector)).to eq built
436
+ end
437
+
438
+ it 'with text' do
439
+ selector = {adjacent: :following, text: 'Third', index: 0}
440
+ built = {xpath: "./following-sibling::*[normalize-space()='Third'][1]"}
441
+
442
+ expect(selector_builder.build(selector)).to eq built
443
+ end
444
+ end
445
+
446
+ describe '#previous_sibling' do
447
+ it 'with no other arguments' do
448
+ selector = {adjacent: :preceding, index: 0}
449
+ built = {xpath: './preceding-sibling::*[1]'}
450
+
451
+ expect(selector_builder.build(selector)).to eq built
452
+ end
453
+
454
+ it 'with index' do
455
+ selector = {adjacent: :preceding, index: 2}
456
+ built = {xpath: './preceding-sibling::*[3]'}
457
+
458
+ expect(selector_builder.build(selector)).to eq built
459
+ end
460
+
461
+ it 'with multiple locators' do
462
+ selector = {adjacent: :preceding, tag_name: 'div', class: 'b', id: true, index: 0}
463
+ built = {xpath: "./preceding-sibling::*[local-name()='div']"\
464
+ "[contains(concat(' ', @class, ' '), ' b ')][@id][1]"}
465
+
466
+ expect(selector_builder.build(selector)).to eq built
467
+ end
468
+
469
+ it 'with text' do
470
+ selector = {adjacent: :preceding, text: 'Second', index: 0}
471
+ built = {xpath: "./preceding-sibling::*[normalize-space()='Second'][1]"}
472
+
473
+ expect(selector_builder.build(selector)).to eq built
474
+ end
475
+ end
476
+
477
+ describe '#child' do
478
+ it 'with no other arguments' do
479
+ selector = {adjacent: :child, index: 0}
480
+ built = {xpath: './child::*[1]'}
481
+
482
+ expect(selector_builder.build(selector)).to eq built
483
+ end
484
+
485
+ it 'with index' do
486
+ selector = {adjacent: :child, index: 2}
487
+ built = {xpath: './child::*[3]'}
488
+
489
+ expect(selector_builder.build(selector)).to eq built
490
+ end
491
+
492
+ it 'with multiple locators' do
493
+ selector = {adjacent: :child, tag_name: 'div', class: 'b', id: true, index: 0}
494
+ built = {xpath: "./child::*[local-name()='div']"\
495
+ "[contains(concat(' ', @class, ' '), ' b ')][@id][1]"}
496
+
497
+ expect(selector_builder.build(selector)).to eq built
498
+ end
499
+
500
+ it 'with text' do
501
+ selector = {adjacent: :child, text: 'Second', index: 0}
502
+ built = {xpath: "./child::*[normalize-space()='Second'][1]"}
503
+
504
+ expect(selector_builder.build(selector)).to eq built
505
+ end
506
+ end
507
+ end
508
+
509
+ context 'with multiple locators' do
510
+ it 'locates using tag name, class, attributes and text' do
511
+ selector = {tag_name: 'div', class: 'content', contenteditable: 'true', text: 'Foo'}
512
+ built = {xpath: ".//*[local-name()='div'][contains(concat(' ', @class, ' '), ' content ')]" \
513
+ "[normalize-space()='Foo'][@contenteditable='true']"}
514
+
515
+ expect(selector_builder.build(selector)).to eq built
516
+ end
517
+ end
518
+
519
+ context 'with simple Regexp' do
520
+ it 'handles spaces' do
521
+ selector = {title: /od Lu/}
522
+ built = {xpath: ".//*[contains(@title, 'od Lu')]"}
523
+
524
+ expect(selector_builder.build(selector)).to eq built
525
+ end
526
+
527
+ it 'handles escaped characters' do
528
+ selector = {src: %r{ages/but}}
529
+ built = {xpath: ".//*[contains(@src, 'ages/but')]"}
530
+
531
+ expect(selector_builder.build(selector)).to eq built
532
+ end
533
+ end
534
+
535
+ context 'with complex Regexp' do
536
+ it 'handles wildcards' do
537
+ selector = {src: /ages.*but/}
538
+ built = {xpath: ".//*[contains(@src, 'ages') and contains(@src, 'but')]", src: /ages.*but/}
539
+
540
+ expect(selector_builder.build(selector)).to eq built
541
+ end
542
+
543
+ it 'handles optional characters' do
544
+ selector = {src: /ages ?but/}
545
+ built = {xpath: ".//*[contains(@src, 'ages') and contains(@src, 'but')]", src: /ages ?but/}
546
+
547
+ expect(selector_builder.build(selector)).to eq built
548
+ end
549
+
550
+ it 'handles anchors' do
551
+ selector = {name: /^new_user_image$/}
552
+ built = {xpath: ".//*[contains(@name, 'new_user_image')]", name: /^new_user_image$/}
553
+
554
+ expect(selector_builder.build(selector)).to eq built
555
+ end
556
+
557
+ it 'handles beginning anchor' do
558
+ selector = {src: /^i/}
559
+ built = {xpath: ".//*[starts-with(@src, 'i')]"}
560
+
561
+ expect(selector_builder.build(selector)).to eq built
562
+ end
563
+
564
+ it 'does not use starts-with if visible locator used' do
565
+ selector = {id: /^vis/, visible_text: 'shown div'}
566
+ built = {xpath: ".//*[contains(@id, 'vis')]", id: /^vis/, visible_text: 'shown div'}
567
+
568
+ expect(selector_builder.build(selector)).to eq built
569
+ end
570
+
571
+ it 'handles case insensitive' do
572
+ selector = {action: /ME/i}
573
+ built = {xpath: './/*[contains(translate(@action,' \
574
+ "'ABCDEFGHIJKLMNOPQRSTUVWXYZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞŸŽŠŒ'," \
575
+ "'abcdefghijklmnopqrstuvwxyzàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿžšœ'), 'me')]"}
576
+
577
+ expect(selector_builder.build(selector)).to eq built
578
+ end
579
+ end
580
+
581
+ context 'with special cased selectors' do
582
+ it 'handles data-* attributes with String' do
583
+ selector = {data_foo: 'user_new'}
584
+ built = {xpath: ".//*[@data-foo='user_new']"}
585
+
586
+ expect(selector_builder.build(selector)).to eq built
587
+ end
588
+
589
+ it 'handles aria-* attributes' do
590
+ selector = {aria_foo: 'user_new'}
591
+ built = {xpath: ".//*[@aria-foo='user_new']"}
592
+
593
+ expect(selector_builder.build(selector)).to eq built
594
+ end
595
+
596
+ it "doesn't modify attribute name when the attribute key is a string" do
597
+ selector = {'_underscore-dash' => 'user_new'}
598
+ built = {xpath: ".//*[@_underscore-dash='user_new']"}
599
+
600
+ expect(selector_builder.build(selector)).to eq built
601
+ end
602
+
603
+ it 'translates ruby attribute names to content attribute names' do
604
+ selector = {http_equiv: 'foo'}
605
+ built = {xpath: ".//*[@http-equiv='foo']"}
606
+
607
+ expect(selector_builder.build(selector)).to eq built
608
+ end
609
+ end
610
+
611
+ context 'returns locators that can not be directly translated' do
612
+ it 'attribute with complicated Regexp at end' do
613
+ selector = {action: /me$/}
614
+ built = {xpath: ".//*[contains(@action, 'me')]", action: /me$/}
615
+
616
+ expect(selector_builder.build(selector)).to eq built
617
+ end
618
+
619
+ it 'class with complicated Regexp' do
620
+ selector = {class: /he?r/}
621
+ built = {xpath: ".//*[contains(@class, 'h') and contains(@class, 'r')]", class: [/he?r/]}
622
+
623
+ expect(selector_builder.build(selector)).to eq built
624
+ end
625
+
626
+ it 'text with any Regexp' do
627
+ selector = {text: /Add/}
628
+ built = {xpath: './/*', text: /Add/}
629
+
630
+ expect(selector_builder.build(selector)).to eq built
631
+ end
632
+
633
+ it 'visible' do
634
+ selector = {tag_name: 'div', visible: true}
635
+ built = {xpath: ".//*[local-name()='div']", visible: true}
636
+
637
+ expect(selector_builder.build(selector)).to eq built
638
+ end
639
+
640
+ it 'not visible' do
641
+ selector = {tag_name: 'span', visible: false}
642
+ built = {xpath: ".//*[local-name()='span']", visible: false}
643
+
644
+ expect(selector_builder.build(selector)).to eq built
645
+ end
646
+
647
+ it 'visible text' do
648
+ selector = {tag_name: 'span', visible_text: 'foo'}
649
+ built = {xpath: ".//*[local-name()='span']", visible_text: 'foo'}
650
+
651
+ expect(selector_builder.build(selector)).to eq built
652
+ end
653
+
654
+ it 'raises exception when visible is not boolean' do
655
+ selector = {visible: 'foo'}
656
+ msg = 'expected one of [TrueClass, FalseClass], got "foo":String'
657
+
658
+ expect { selector_builder.build(selector) }.to raise_exception TypeError, msg
659
+ end
660
+
661
+ it 'raises exception when visible text is not a String or Regexp' do
662
+ selector = {visible_text: 7}
663
+ msg = /expected one of \[String, Regexp\], got 7:(Fixnum|Integer)/
664
+
665
+ expect { selector_builder.build(selector) }.to raise_exception TypeError, msg
666
+ end
667
+ end
668
+
669
+ context 'with element scope' do
670
+ let(:query_scope) { instance_double Watir::HTMLElement }
671
+ let(:scope_built) { @scope_built || {xpath: ".//*[local-name()='div'][@id='table-rows-test']"} }
672
+
673
+ before do
674
+ allow(query_scope).to receive(:selector_builder).and_return(selector_builder)
675
+ allow(query_scope).to receive(:browser).and_return(browser)
676
+ allow(selector_builder).to receive(:built).and_return(scope_built)
677
+ end
678
+
679
+ it 'uses scope' do
680
+ selector = {tag_name: 'div'}
681
+ built = {xpath: "(#{scope_built[:xpath]})[1]//*[local-name()='div']"}
682
+
683
+ expect(selector_builder.build(selector)).to eq built
684
+ end
685
+
686
+ it 'does not use scope if selector is a CSS' do
687
+ selector = {css: 'div'}
688
+
689
+ build_selector = selector_builder.build(selector)
690
+ expect(build_selector.delete(:scope)).to_not be_nil
691
+ expect(build_selector).to eq selector
692
+ end
693
+
694
+ it 'does not use scope if selector is a XPath' do
695
+ selector = {xpath: './/*'}
696
+
697
+ build_selector = selector_builder.build(selector)
698
+ expect(build_selector.delete(:scope)).to_not be_nil
699
+ expect(build_selector).to eq selector
700
+ end
701
+
702
+ it 'does not use scope if selector has :adjacent' do
703
+ selector = {adjacent: :ancestor, index: 0}
704
+ built = {xpath: './ancestor::*[1]'}
705
+
706
+ build_selector = selector_builder.build(selector)
707
+ expect(build_selector.delete(:scope)).to_not be_nil
708
+ expect(build_selector).to eq built
709
+ end
710
+
711
+ it 'does not use scope if query_scope built has multiple keys' do
712
+ selector = {tag_name: 'div'}
713
+ built = {xpath: ".//*[local-name()='div']"}
714
+ scope_built[:visible] = true
715
+
716
+ build_selector = selector_builder.build(selector)
717
+ expect(build_selector.delete(:scope)).to_not be_nil
718
+ expect(build_selector).to eq built
719
+ end
720
+ end
721
+
722
+ context 'with specific scope' do
723
+ let(:scope_built) { {xpath: ".//*[local-name()='iframe'][@id='one']"} }
724
+
725
+ it 'does not use scope if query scope is an IFrame' do
726
+ query_scope = instance_double Watir::IFrame
727
+ selector_builder = described_class.new(attributes, query_scope)
728
+
729
+ allow(selector_builder).to receive(:built).and_return(scope_built)
730
+ allow(query_scope).to receive(:selector_builder).and_return(selector_builder)
731
+ allow(query_scope).to receive(:browser).and_return(browser)
732
+ allow(query_scope).to receive(:is_a?).with(Watir::Browser).and_return(false)
733
+ allow(query_scope).to receive(:is_a?).with(Watir::IFrame).and_return(true)
734
+
735
+ selector = {tag_name: 'div'}
736
+ built = {xpath: ".//*[local-name()='div']"}
737
+
738
+ build_selector = selector_builder.build(selector)
739
+ expect(build_selector.delete(:scope)).to_not be_nil
740
+ expect(build_selector).to eq built
741
+ end
742
+ end
743
+ end
744
+ end