xpath 0.1.4 → 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/xpath/html.rb CHANGED
@@ -3,112 +3,151 @@ module XPath
3
3
  include XPath
4
4
  extend self
5
5
 
6
- def link(locator, options={})
7
- href = options[:href]
8
- link = descendant(:a)[href ? attr(:href).equals(href) : attr(:href)]
9
- link[attr(:id).equals(locator) | string.n.is(locator) | attr(:title).is(locator) | descendant(:img)[attr(:alt).is(locator)]]
10
- end
11
-
12
- def content(locator)
13
- child(:"descendant-or-self::*")[current.n.contains(locator)]
14
- end
15
-
6
+ # Match an `a` link element.
7
+ #
8
+ # @param [String] locator
9
+ # Text, id, title, or image alt attribute of the link
10
+ #
11
+ def link(locator)
12
+ link = descendant(:a)[attr(:href)]
13
+ link[attr(:id).equals(locator) | string.n.contains(locator) | attr(:title).contains(locator) | descendant(:img)[attr(:alt).contains(locator)]]
14
+ end
15
+
16
+ # Match a `submit`, `image`, or `button` element.
17
+ #
18
+ # @param [String] locator
19
+ # Value, title, id, or image alt attribute of the button
20
+ #
16
21
  def button(locator)
17
- button = descendant(:input)[attr(:type).one_of('submit', 'image', 'button')][attr(:id).equals(locator) | attr(:value).is(locator) | attr(:title).is(locator)]
18
- button += descendant(:button)[attr(:id).equals(locator) | attr(:value).is(locator) | string.n.is(locator) | attr(:title).is(locator)]
19
- button += descendant(:input)[attr(:type).equals('image')][attr(:alt).is(locator)]
22
+ button = descendant(:input)[attr(:type).one_of('submit', 'reset', 'image', 'button')][attr(:id).equals(locator) | attr(:value).contains(locator) | attr(:title).contains(locator)]
23
+ button += descendant(:button)[attr(:id).equals(locator) | attr(:value).contains(locator) | string.n.contains(locator) | attr(:title).contains(locator)]
24
+ button += descendant(:input)[attr(:type).equals('image')][attr(:alt).contains(locator)]
20
25
  end
21
26
 
27
+
28
+ # Match anything returned by either {#link} or {#button}.
29
+ #
30
+ # @param [String] locator
31
+ # Text, id, title, or image alt attribute of the link or button
32
+ #
22
33
  def link_or_button(locator)
23
34
  link(locator) + button(locator)
24
35
  end
25
36
 
37
+
38
+ # Match any `fieldset` element.
39
+ #
40
+ # @param [String] locator
41
+ # Legend or id of the fieldset
42
+ #
26
43
  def fieldset(locator)
27
- descendant(:fieldset)[attr(:id).equals(locator) | descendant(:legend)[string.n.is(locator)]]
44
+ descendant(:fieldset)[attr(:id).equals(locator) | child(:legend)[string.n.contains(locator)]]
28
45
  end
29
46
 
30
- def field(locator, options={})
47
+
48
+ # Match any `input`, `textarea`, or `select` element that doesn't have a
49
+ # type of `submit`, `image`, or `hidden`.
50
+ #
51
+ # @param [String] locator
52
+ # Label, id, or name of field to match
53
+ #
54
+ def field(locator)
31
55
  xpath = descendant(:input, :textarea, :select)[~attr(:type).one_of('submit', 'image', 'hidden')]
32
56
  xpath = locate_field(xpath, locator)
33
- xpath = xpath[attr(:checked)] if options[:checked]
34
- xpath = xpath[~attr(:checked)] if options[:unchecked]
35
- xpath = xpath[field_value(options[:with])] if options.has_key?(:with)
36
57
  xpath
37
58
  end
38
59
 
39
- def fillable_field(locator, options={})
60
+
61
+ # Match any `input` or `textarea` element that can be filled with text.
62
+ # This excludes any inputs with a type of `submit`, `image`, `radio`,
63
+ # `checkbox`, `hidden`, or `file`.
64
+ #
65
+ # @param [String] locator
66
+ # Label, id, or name of field to match
67
+ #
68
+ def fillable_field(locator)
40
69
  xpath = descendant(:input, :textarea)[~attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')]
41
70
  xpath = locate_field(xpath, locator)
42
- xpath = xpath[field_value(options[:with])] if options.has_key?(:with)
43
71
  xpath
44
72
  end
45
73
 
46
- def select(locator, options={})
47
- xpath = locate_field(descendant(:select), locator)
48
74
 
49
- options[:options].each do |option|
50
- xpath = xpath[descendant(:option).equals(option)]
51
- end if options[:options]
75
+ # Match any `select` element.
76
+ #
77
+ # @param [String] locator
78
+ # Label, id, or name of the field to match
79
+ #
80
+ def select(locator)
81
+ locate_field(descendant(:select), locator)
82
+ end
52
83
 
53
- [options[:selected]].flatten.each do |option|
54
- xpath = xpath[descendant(:option)[attr(:selected)].equals(option)]
55
- end if options[:selected]
56
84
 
57
- xpath
85
+ # Match any `input` element of type `checkbox`.
86
+ #
87
+ # @param [String] locator
88
+ # Label, id, or name of the checkbox to match
89
+ #
90
+ def checkbox(locator)
91
+ locate_field(descendant(:input)[attr(:type).equals('checkbox')], locator)
58
92
  end
59
93
 
60
- def checkbox(locator, options={})
61
- xpath = locate_field(descendant(:input)[attr(:type).equals('checkbox')], locator)
62
- end
63
94
 
64
- def radio_button(locator, options={})
95
+ # Match any `input` element of type `radio`.
96
+ #
97
+ # @param [String] locator
98
+ # Label, id, or name of the radio button to match
99
+ #
100
+ def radio_button(locator)
65
101
  locate_field(descendant(:input)[attr(:type).equals('radio')], locator)
66
102
  end
67
103
 
68
- def file_field(locator, options={})
104
+
105
+ # Match any `input` element of type `file`.
106
+ #
107
+ # @param [String] locator
108
+ # Label, id, or name of the file field to match
109
+ #
110
+ def file_field(locator)
69
111
  locate_field(descendant(:input)[attr(:type).equals('file')], locator)
70
112
  end
71
113
 
114
+
115
+ # Match an `optgroup` element.
116
+ #
117
+ # @param [String] name
118
+ # Label for the option group
119
+ #
72
120
  def optgroup(name)
73
- descendant(:optgroup)[attr(:label).is(name)]
121
+ descendant(:optgroup)[attr(:label).contains(name)]
74
122
  end
75
123
 
76
- def option(name)
77
- descendant(:option)[string.n.is(name)]
78
- end
79
124
 
80
- def table(locator, options={})
81
- xpath = descendant(:table)[attr(:id).equals(locator) | descendant(:caption).contains(locator)]
82
- xpath = xpath[table_rows(options[:rows])] if options[:rows]
83
- xpath
125
+ # Match an `option` element.
126
+ #
127
+ # @param [String] name
128
+ # Visible text of the option
129
+ #
130
+ def option(name)
131
+ descendant(:option)[string.n.equals(name)]
84
132
  end
85
133
 
86
- def table_rows(rows)
87
- row_conditions = descendant(:tr)[table_row(rows.first)]
88
- rows.drop(1).each do |row|
89
- row_conditions = row_conditions.next_sibling(:tr)[table_row(row)]
90
- end
91
- row_conditions
92
- end
93
134
 
94
- def table_row(cells)
95
- cell_conditions = child(:td, :th)[string.n.equals(cells.first)]
96
- cells.drop(1).each do |cell|
97
- cell_conditions = cell_conditions.next_sibling(:td, :th)[string.n.equals(cell)]
98
- end
99
- cell_conditions
135
+ # Match any `table` element.
136
+ #
137
+ # @param [String] locator
138
+ # Caption or id of the table to match
139
+ # @option options [Array] :rows
140
+ # Content of each cell in each row to match
141
+ #
142
+ def table(locator)
143
+ descendant(:table)[attr(:id).equals(locator) | descendant(:caption).contains(locator)]
100
144
  end
101
145
 
102
146
  protected
103
147
 
104
148
  def locate_field(xpath, locator)
105
- locate_field = xpath[attr(:id).equals(locator) | attr(:name).equals(locator) | attr(:id).equals(anywhere(:label)[string.n.is(locator)].attr(:for))]
106
- locate_field += descendant(:label)[string.n.is(locator)].descendant(xpath)
149
+ locate_field = xpath[attr(:id).equals(locator) | attr(:name).equals(locator) | attr(:placeholder).equals(locator) | attr(:id).equals(anywhere(:label)[string.n.contains(locator)].attr(:for))]
150
+ locate_field += descendant(:label)[string.n.contains(locator)].descendant(xpath)
107
151
  end
108
-
109
- def field_value(value)
110
- (string.n.is(value) & tag(:textarea)) | (attr(:value).equals(value) & ~tag(:textarea))
111
- end
112
-
113
152
  end
114
153
  end
@@ -0,0 +1,8 @@
1
+ module XPath
2
+ class Literal
3
+ attr_reader :value
4
+ def initialize(value)
5
+ @value = value
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,146 @@
1
+ module XPath
2
+ class Renderer
3
+ def self.render(node)
4
+ new.render(node)
5
+ end
6
+
7
+ def render(node)
8
+ arguments = node.arguments.map { |argument| convert_argument(argument) }
9
+ send(node.expression, *arguments)
10
+ end
11
+
12
+ def convert_argument(argument)
13
+ case argument
14
+ when Expression, Union then render(argument)
15
+ when Array then argument.map { |element| convert_argument(element) }
16
+ when String then string_literal(argument)
17
+ when Literal then argument.value
18
+ else argument.to_s
19
+ end
20
+ end
21
+
22
+ def string_literal(string)
23
+ if string.include?("'")
24
+ string = string.split("'", -1).map do |substr|
25
+ "'#{substr}'"
26
+ end.join(%q{,"'",})
27
+ "concat(#{string})"
28
+ else
29
+ "'#{string}'"
30
+ end
31
+ end
32
+
33
+ def this_node
34
+ '.'
35
+ end
36
+
37
+ def descendant(parent, element_names)
38
+ if element_names.length == 1
39
+ "#{parent}//#{element_names.first}"
40
+ elsif element_names.length > 1
41
+ "#{parent}//*[#{element_names.map { |e| "self::#{e}" }.join(" | ")}]"
42
+ else
43
+ "#{parent}//*"
44
+ end
45
+ end
46
+
47
+ def child(parent, element_names)
48
+ if element_names.length == 1
49
+ "#{parent}/#{element_names.first}"
50
+ elsif element_names.length > 1
51
+ "#{parent}/*[#{element_names.map { |e| "self::#{e}" }.join(" | ")}]"
52
+ else
53
+ "#{parent}/*"
54
+ end
55
+ end
56
+
57
+ def axis(parent, name, tag_name)
58
+ "#{parent}/#{name}::#{tag_name}"
59
+ end
60
+
61
+ def node_name(current)
62
+ "name(#{current})"
63
+ end
64
+
65
+ def where(on, condition)
66
+ "#{on}[#{condition}]"
67
+ end
68
+
69
+ def attribute(current, name)
70
+ "#{current}/@#{name}"
71
+ end
72
+
73
+ def equality(one, two)
74
+ "#{one} = #{two}"
75
+ end
76
+
77
+ def variable(name)
78
+ "%{#{name}}"
79
+ end
80
+
81
+ def text(current)
82
+ "#{current}/text()"
83
+ end
84
+
85
+ def normalized_space(current)
86
+ "normalize-space(#{current})"
87
+ end
88
+
89
+ def literal(node)
90
+ node
91
+ end
92
+
93
+ def css(current, selector)
94
+ paths = Nokogiri::CSS.xpath_for(selector).map do |xpath_selector|
95
+ "#{current}#{xpath_selector}"
96
+ end
97
+ union(paths)
98
+ end
99
+
100
+ def union(*expressions)
101
+ expressions.join(' | ')
102
+ end
103
+
104
+ def anywhere(tag_name)
105
+ "//#{tag_name}"
106
+ end
107
+
108
+ def contains(current, value)
109
+ "contains(#{current}, #{value})"
110
+ end
111
+
112
+ def starts_with(current, value)
113
+ "starts-with(#{current}, #{value})"
114
+ end
115
+
116
+ def and(one, two)
117
+ "(#{one} and #{two})"
118
+ end
119
+
120
+ def or(one, two)
121
+ "(#{one} or #{two})"
122
+ end
123
+
124
+ def one_of(current, values)
125
+ values.map { |value| "#{current} = #{value}" }.join(' or ')
126
+ end
127
+
128
+ def next_sibling(current, element_names)
129
+ if element_names.length == 1
130
+ "#{current}/following-sibling::*[1]/self::#{element_names.first}"
131
+ elsif element_names.length > 1
132
+ "#{current}/following-sibling::*[1]/self::*[#{element_names.map { |e| "self::#{e}" }.join(" | ")}]"
133
+ else
134
+ "#{current}/following-sibling::*[1]/self::*"
135
+ end
136
+ end
137
+
138
+ def inverse(current)
139
+ "not(#{current})"
140
+ end
141
+
142
+ def string_function(current)
143
+ "string(#{current})"
144
+ end
145
+ end
146
+ end
data/lib/xpath/union.rb CHANGED
@@ -3,29 +3,27 @@ module XPath
3
3
  include Enumerable
4
4
 
5
5
  attr_reader :expressions
6
+ alias_method :arguments, :expressions
6
7
 
7
8
  def initialize(*expressions)
8
9
  @expressions = expressions
9
10
  end
10
11
 
11
- def each(&block)
12
- expressions.each(&block)
13
- end
14
-
15
- def to_s
16
- to_xpaths.join(' | ')
12
+ def expression
13
+ :union
17
14
  end
18
15
 
19
- def to_xpath(predicate=nil)
20
- expressions.map { |e| e.to_xpath(predicate) }.join(' | ')
16
+ def each(&block)
17
+ arguments.each(&block)
21
18
  end
22
19
 
23
- def to_xpaths
24
- [to_xpath(:exact), to_xpath(:fuzzy)].uniq
20
+ def method_missing(*args)
21
+ XPath::Union.new(*arguments.map { |e| e.send(*args) })
25
22
  end
26
23
 
27
- def method_missing(*args)
28
- XPath::Union.new(*expressions.map { |e| e.send(*args) })
24
+ def to_xpath
25
+ Renderer.render(self)
29
26
  end
27
+ alias_method :to_s, :to_xpath
30
28
  end
31
29
  end
data/lib/xpath/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module XPath
2
- VERSION = '0.1.4'
2
+ VERSION = '1.0.0.beta1'
3
3
  end
@@ -28,7 +28,15 @@
28
28
  <input type="submit" title="My submit title" value="submit-with-title" data="title-submit">
29
29
  <input type="submit" title="Exact submit title" value="exact title submit" data="exact-title-submit">
30
30
  <input type="submit" title="Not Exact submit title" value="exact title submit" data="not-exact-title-submit">
31
-
31
+
32
+ <input type="reset" id="reset-with-id" data="id-reset" value="Has ID"/>
33
+ <input type="reset" value="reset-with-value" data="value-reset"/>
34
+ <input type="reset" value="not exact value reset" data="not-exact-value-reset"/>
35
+ <input type="reset" value="exact value reset" data="exact-value-reset"/>
36
+ <input type="reset" title="My reset title" value="reset-with-title" data="title-reset">
37
+ <input type="reset" title="Exact reset title" value="exact title reset" data="exact-title-reset">
38
+ <input type="reset" title="Not Exact reset title" value="exact title reset" data="not-exact-title-reset">
39
+
32
40
  <input type="button" id="button-with-id" data="id-button" value="Has ID"/>
33
41
  <input type="button" value="button-with-value" data="value-button"/>
34
42
  <input type="button" value="not exact value button" data="not-exact-value-button"/>
@@ -46,10 +54,10 @@
46
54
  <input type="image" title="Not Exact imgbut title" value="not exact title imgbut" data="not-exact-title-imgbut">
47
55
  <input type="image" title="Exact imgbut title" value="exact title imgbut" data="exact-title-imgbut">
48
56
 
49
- <button id="btag-with-id" data="id-btag" value="Has ID"/>
50
- <button value="btag-with-value" data="value-btag"/>
51
- <button value="not exact value btag" data="not-exact-value-btag"/>
52
- <button value="exact value btag" data="exact-value-btag"/>
57
+ <button id="btag-with-id" data="id-btag" value="Has ID"></button>
58
+ <button value="btag-with-value" data="value-btag"></button>
59
+ <button value="not exact value btag" data="not-exact-value-btag"></button>
60
+ <button value="exact value btag" data="exact-value-btag"></button>
53
61
 
54
62
  <button data="text-btag">btag-with-text</button>
55
63
  <button data="not-exact-text-btag">not exact text btag</button>
@@ -77,16 +85,30 @@
77
85
  <fieldset data="fieldset-legend-span"><legend><span>Span Legend</span></legend></fieldset>
78
86
  <fieldset data="fieldset-fuzzy"><legend>Long legend yo</legend></fieldset>
79
87
  <fieldset data="fieldset-exact"><legend>Long legend</legend></fieldset>
88
+ <fieldset data="fieldset-outer"><legend>Outer legend</legend>
89
+ <fieldset data="fieldset-inner"><legend>Inner legend</legend></fieldset>
90
+ </fieldset>
80
91
  </p>
81
92
 
82
93
  <p>
83
94
  <select>
84
95
  <optgroup label="Group A" data="optgroup-a"></optgroup>
85
96
  <optgroup label="Group B" data="optgroup-b"></optgroup>
97
+ <option data="option-with-text-data">Option with text</option>
86
98
  </select>
87
99
  </p>
88
100
 
89
- <p>
101
+ <h2>Tables</h2>
102
+
103
+ <table id="table-with-id" data="table-with-id-data">
104
+ <tr><td>First</td><td>Second</td></tr>
105
+ </table>
106
+
107
+ <table data="table-with-caption-data">
108
+ <caption>Table with caption</caption>
109
+ <tr><td>First</td><td>Second</td></tr>
110
+ </table>
111
+
90
112
  <table id="whitespaced-table" data="table-with-whitespace">
91
113
  <tr>
92
114
  <td data="cell-whitespaced">I have
@@ -95,7 +117,7 @@
95
117
  <td>I don't</td>
96
118
  </tr>
97
119
  </table>
98
- </p>
120
+
99
121
 
100
122
  <p>
101
123
  <h2>Fields</h2>
@@ -103,6 +125,7 @@
103
125
  <h4>With id</h4>
104
126
  <input id="input-with-id" value="correct-value" data="input-with-id-data"/>
105
127
  <input type="text" id="input-text-with-id" data="input-text-with-id-data"/>
128
+ <input type="file" id="input-file-with-id" data="input-file-with-id-data"/>
106
129
  <input type="password" id="input-password-with-id" data="input-password-with-id-data"/>
107
130
  <input type="custom" id="input-custom-with-id" data="input-custom-with-id-data"/>
108
131
  <textarea id="textarea-with-id" data="textarea-with-id-data">Correct value</textarea>
@@ -110,10 +133,13 @@
110
133
  <input type="submit" id="input-submit-with-id" data="input-submit-with-id-data"/>
111
134
  <input type="image" id="input-image-with-id" data="input-image-with-id-data"/>
112
135
  <input type="hidden" id="input-hidden-with-id" data="input-hidden-with-id-data"/>
136
+ <input type="checkbox" id="input-checkbox-with-id" data="input-checkbox-with-id-data"/>
137
+ <input type="radio" id="input-radio-with-id" data="input-radio-with-id-data"/>
113
138
 
114
139
  <h4>With name</h4>
115
140
  <input name="input-with-name" data="input-with-name-data"/>
116
141
  <input type="text" name="input-text-with-name" data="input-text-with-name-data"/>
142
+ <input type="file" name="input-file-with-name" data="input-file-with-name-data"/>
117
143
  <input type="password" name="input-password-with-name" data="input-password-with-name-data"/>
118
144
  <input type="custom" name="input-custom-with-name" data="input-custom-with-name-data"/>
119
145
  <textarea name="textarea-with-name" data="textarea-with-name-data"></textarea>
@@ -121,10 +147,21 @@
121
147
  <input type="submit" name="input-submit-with-name" data="input-submit-with-name-data"/>
122
148
  <input type="image" name="input-image-with-name" data="input-image-with-name-data"/>
123
149
  <input type="hidden" name="input-hidden-with-name" data="input-hidden-with-name-data"/>
150
+ <input type="checkbox" name="input-checkbox-with-name" data="input-checkbox-with-name-data"/>
151
+ <input type="radio" name="input-radio-with-name" data="input-radio-with-name-data"/>
152
+
153
+ <h4>With placeholder</h4>
154
+ <input name="input-with-placeholder" data="input-with-placeholder-data"/>
155
+ <input type="text" placeholder="input-text-with-placeholder" data="input-text-with-placeholder-data"/>
156
+ <input type="password" placeholder="input-password-with-placeholder" data="input-password-with-placeholder-data"/>
157
+ <input type="custom" placeholder="input-custom-with-placeholder" data="input-custom-with-placeholder-data"/>
158
+ <textarea placeholder="textarea-with-placeholder" data="textarea-with-placeholder-data"></textarea>
159
+ <input type="hidden" placeholder="input-hidden-with-placeholder" data="input-hidden-with-placeholder-data"/>
124
160
 
125
161
  <h4>With referenced label</h4>
126
162
  <label for="input-with-label">Input with label</label><input id="input-with-label" data="input-with-label-data"/>
127
163
  <label for="input-text-with-label">Input text with label</label><input type="text" id="input-text-with-label" data="input-text-with-label-data"/>
164
+ <label for="input-file-with-label">Input file with label</label><input type="file" id="input-file-with-label" data="input-file-with-label-data"/>
128
165
  <label for="input-password-with-label">Input password with label</label><input type="password" id="input-password-with-label" data="input-password-with-label-data"/>
129
166
  <label for="input-custom-with-label">Input custom with label</label><input type="custom" id="input-custom-with-label" data="input-custom-with-label-data"/>
130
167
  <label for="textarea-with-label">Textarea with label</label><textarea id="textarea-with-label" data="textarea-with-label-data"></textarea>
@@ -132,10 +169,13 @@
132
169
  <label for="input-submit-with-label">Input submit with label</label><input type="submit" id="input-submit-with-label" data="input-submit-with-label-data"/>
133
170
  <label for="input-image-with-label">Input image with label</label><input type="image" id="input-image-with-label" data="input-image-with-label-data"/>
134
171
  <label for="input-hidden-with-label">Input hidden with label</label><input type="hidden" id="input-hidden-with-label" data="input-hidden-with-label-data"/>
172
+ <label for="input-checkbox-with-label">Input checkbox with label</label><input type="checkbox" id="input-checkbox-with-label" data="input-checkbox-with-label-data"/>
173
+ <label for="input-radio-with-label">Input radio with label</label><input type="radio" id="input-radio-with-label" data="input-radio-with-label-data"/>
135
174
 
136
175
  <h4>With parent label</h4>
137
176
  <label>Input with parent label<input data="input-with-parent-label-data"/></label>
138
177
  <label>Input text with parent label<input type="text" data="input-text-with-parent-label-data"/></label>
178
+ <label>Input file with parent label<input type="file" data="input-file-with-parent-label-data"/></label>
139
179
  <label>Input password with parent label<input type="password" data="input-password-with-parent-label-data"/></label>
140
180
  <label>Input custom with parent label<input type="custom" data="input-custom-with-parent-label-data"/></label>
141
181
  <label>Textarea with parent label<textarea data="textarea-with-parent-label-data"></textarea></label>
@@ -143,4 +183,6 @@
143
183
  <label>Input submit with parent label<input type="submit" data="input-submit-with-parent-label-data"/></label>
144
184
  <label>Input image with parent label<input type="image" data="input-image-with-parent-label-data"/></label>
145
185
  <label>Input hidden with parent label<input type="hidden" data="input-hidden-with-parent-label-data"/></label>
186
+ <label>Input checkbox with parent label<input type="checkbox" data="input-checkbox-with-parent-label-data"/></label>
187
+ <label>Input radio with parent label<input type="radio" data="input-radio-with-parent-label-data"/></label>
146
188
  </p>