xpath 0.1.4 → 1.0.0.beta1

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.
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>