Module: Rsel::Support

Included in:
SeleniumTest, StudyHtml
Defined in:
lib/rsel/support.rb

Overview

Support functions for Rsel

Instance Method Summary collapse

Instance Method Details

#apply_scope(container, inner) ⇒ Object

Restrict the scope of all XPath expressions in inner by prepending container to each of them, and return new union expression where inner matches only if it's a child of container.



124
125
126
127
128
129
# File 'lib/rsel/support.rb', line 124

def apply_scope(container, inner)
  scoped_expressions = xpath_expressions(inner).collect do |expr|
    container.child(expr)
  end
  return XPath::Union.new(*scoped_expressions).to_s
end

#csspath(locator, scope = {}) ⇒ Object

Return a Selenium-style CSS extending the given CSS with a locator.

Examples:

xpath('link', 'Log in')
xpath('button', 'Submit')
xpath('field', 'First name')
xpath('table_row', ['First', 'Last'])

Parameters:

  • locator (String)

    A Selenium-style locator beginning with css=. All other values are ignored. (Meaning the locator is returned as passed.)

  • scope (Hash) (defaults to: {})

    Keywords to restrict the scope of matching elements

Options Hash (scope):

  • :within (String)

    Restrict scope to elements having this id, matching locator only if it's contained within an element with this id.

  • :in_row (String)

    Restrict scope to a table row containing this text, matching locator only if that locator is in the same row as the given text.



67
68
69
70
71
72
73
74
75
# File 'lib/rsel/support.rb', line 67

def csspath(locator, scope={})
  return locator unless locator =~ /^css=/ && scope != {}
  if scope[:within]
    locator[4,0] = "##{scope[:within]} "
  elsif scope[:in_row]
    locator[4,0] = "tr:contains(\"#{scope[:in_row]}\") "
  end
  return locator
end

#escape_for_hash(text) ⇒ Object

Escape certain characters to generate characters that can't otherwise be used in FitNesse hashtables.

  • \; becomes :
  • \' becomes ,
  • [ becomes {
  • ] becomes }
  • \ becomes \

Since:

  • 0.1.1



164
165
166
167
168
169
170
171
172
# File 'lib/rsel/support.rb', line 164

def escape_for_hash(text)
  # ((?:\\\\)*) allows any extra pairs of "\"s to be saved.
  text = text.gsub(/(^|[^\\])\\((?:\\\\)*);/, '\1\2:')
  text = text.gsub(/(^|[^\\])\\((?:\\\\)*)'/, '\1\2,')
  text = text.gsub(/(^|[^\\])\\((?:\\\\)*)\[/, '\1\2{')
  text = text.gsub(/(^|[^\\])\\((?:\\\\)*)\]/, '\1\2}')
  text = text.gsub(/\\\\/, '\\')
  return text
end

#failed_within(seconds, &block) ⇒ Boolean

Ensure that a given block fails within a timeout.

This is a kind of counterpart to #result_within

Parameters:

  • seconds (Integer, String)

    Integer number of seconds to keep retrying the block

  • block (Block)

    Any block of code that might evaluate to a false value, or raise an exception, within the timeout.

Returns:

  • (Boolean)

    true if the block failed (returned false/nil or raised an exception) within the timeout, false if the block never failed within the timeout.

Since:

  • 0.1.2



354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/rsel/support.rb', line 354

def failed_within(seconds, &block)
  (seconds.to_i + 1).times do
    begin
      result = yield
    rescue
      return true
    else
      return true if !result
    end
    sleep 1
  end
  return false
end

#globify(text) ⇒ Object

Return text with glob markers * on each end, unless the text begins with exact:, regexp:, or regexpi:. This effectively allows normal text to match as a "contains" search instead of matching the entire string.

Parameters:

  • text (String)

    Text to globify

Since:

  • 0.1.2



292
293
294
295
296
297
298
# File 'lib/rsel/support.rb', line 292

def globify(text)
  if /^(exact|regexpi?):/ === text
    return text
  else
    return text.sub(/^(glob:)?\*?/, '*').sub(/\*?$/, '*')
  end
end

#loc(locator, kind = '', scope = {}) ⇒ Object

Convert the given locator to a format accepted by Selenium. If locator starts with id=, name=, dom=, xpath= link= or css=, then the locator is returned unchanged. Otherwise, locator is assumed to be a plain string, and a Selenium-style xpath= locator is returned, matching HTML elements of the given kind, in the given scope. This allows you to use simple human-readable locator strings, or more specific Selenium-style locators.

Parameters:

  • locator (String)

    A Selenium-style locator beginning with id=, name=, dom=, xpath=, link= or css=, or a plain text string that will automatically match on the id, name, label, value, or text content depending on the value of kind.

  • kind (String) (defaults to: '')

    What kind of locator you're using (link, button, checkbox, field etc.). This must correspond to a method name in XPath::HTML. If you're using a raw Selenium-style locator string, this argument can be omitted.

  • scope (Hash) (defaults to: {})

    Keywords to restrict the scope of matching elements. Ignored when a Selenium-style locator is used. See the #xpath documentation for allowed options.



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/rsel/support.rb', line 31

def loc(locator, kind='', scope={})
  if locator.empty?
    raise ArgumentError, "locator is required."
  elsif locator =~ /^css=/ && scope != {}
    return csspath(locator, scope)
  elsif locator =~ /^(id=|name=|dom=|xpath=|link=|css=)/
    return locator
  else
    if kind.empty?
      raise ArgumentError, "kind is required for Rsel-style locators"
    else
      return xpath(kind, locator, scope)
    end
  end
end

#normalize_ids(ids) ⇒ Object

Normalize the given hash of name => locator mappings. Converts all keys to lowercase and calls #escape_for_hash on them.

Since:

  • 0.1.1



180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/rsel/support.rb', line 180

def normalize_ids(ids)
  ids = {} unless ids.is_a? Hash
  ids.keys.each do |key|
    new_key = escape_for_hash(key.to_s.downcase)
    new_value = escape_for_hash(ids[key])
    ids[new_key] = new_value

    # Delete the old key if necessary
    if new_key != key
      ids.delete(key)
    end
  end
end

#result_within(seconds, &block) ⇒ Object

Ensure that a given block gets a result within a timeout.

This executes the given block statement once per second, until it returns a value that evaluates as true (meaning anything other than false or nil), or until the seconds timeout is reached. If the block evaluates as true within the timeout, return the block result. Otherwise, return nil.

If the block never returns a value other than false or nil, then return nil. If the block raises an exception (any exception), that's considered a false result, and the block will be retried until a true result is returned, or the seconds timeout is reached.

TODO: Return false if the block takes too long to execute (and exceeds the timeout)

Parameters:

  • seconds (Integer, String)

    Integer number of seconds to keep retrying the block

  • block (Block)

    Any block of code that might evaluate to a non-false value

Returns:

  • Result of the block if it evaluated true-ish within the timeout, nil if the block always evaluated as false or raised an exception.

Since:

  • 0.1.2



328
329
330
331
332
333
334
335
# File 'lib/rsel/support.rb', line 328

def result_within(seconds, &block)
  (seconds.to_i + 1).times do
    result = yield rescue nil
    return result if result
    sleep 1
  end
  return nil
end

#selenium_compare(text, expected) ⇒ Object

Compare values like Selenium does, with regexpi? and globs.

Parameters:

  • text (String)

    A string.

  • expected (String)

    Another string. This one may have glob:, regexp:, etc.

Since:

  • 0.1.1



268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/rsel/support.rb', line 268

def selenium_compare(text, expected)
  if expected.sub!(/^regexp:/, '')
    return /#{expected}/ === text
  elsif expected.sub!(/^regexpi:/, '')
    return /#{expected}/i === text
  elsif expected.sub!(/^exact:/, '')
    return text == expected
  else
    # Default is glob, whether or not glob: is present.
    expected.sub!(/^glob:/, '')
    return File.fnmatch(expected, text)
  end
end

#string_is_true?(s) ⇒ Boolean

Convert a string like "yes", "true", "1", etc. Values currently recognized as true, case-insensitive:

  • [empty string]
  • 1
  • Check
  • Checked
  • On
  • Select
  • Selected
  • True
  • Yes

Returns:

  • (Boolean)

Since:

  • 0.1.1



255
256
257
# File 'lib/rsel/support.rb', line 255

def string_is_true?(s)
  return /^(?:yes|true|on|(?:check|select)(?:ed)?|1|)$/i === s
end

#strip_tags(text) ⇒ Object

Strip HTML tags from the given text. This can be used for converting URLs that FitNesse has marked up back into plain URLs.

Parameters:

  • text (String)

    Text, possibly including markup, that you want to strip

Since:

  • 0.1.1



203
204
205
# File 'lib/rsel/support.rb', line 203

def strip_tags(text)
  return text.gsub(/<\/?[^>]*>/, '')
end

#xpath(kind, locator, scope = {}) ⇒ Object

Return a Selenium-style xpath generated by calling XPath::HTML.<kind> with the given locator. If scope options are provided, the xpath is modified accordingly, to match only elements in the given scope.

Examples:

xpath('link', 'Log in')
xpath('button', 'Submit')
xpath('field', 'First name')
xpath('table_row', ['First', 'Last'])

Parameters:

  • kind (String)

    What kind of locator you're using (link, button, checkbox, field etc.). This must correspond to a method name in XPath::HTML.

  • locator (String)

    Name, id, value, label or whatever other locators are accepted by XPath::HTML.<kind>

  • scope (Hash) (defaults to: {})

    Keywords to restrict the scope of matching elements

Options Hash (scope):

  • :within (String)

    Restrict scope to elements having this id, matching locator only if it's contained within an element with this id.

  • :in_row (String)

    Restrict scope to a table row containing this text, matching locator only if that locator is in the same row as the given text.



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/rsel/support.rb', line 102

def xpath(kind, locator, scope={})
  if !XPath::HTML.respond_to?(kind)
    raise ArgumentError, "Unknown kind of locator: '#{kind}'"
  end
  loc_xp = XPath::HTML.send(kind, locator)
  if scope[:within]
    parent = XPath.descendant[XPath.attr(:id).equals(scope[:within])]
    result = apply_scope(parent, loc_xp)
  elsif scope[:in_row]
    row = XPath.descendant(:tr)[XPath.contains(scope[:in_row])]
    result = apply_scope(row, loc_xp)
  else
    result = loc_xp.to_s
  end
  return "xpath=#{result}"
end

#xpath_expressions(union) ⇒ Object

Return an array of individual Expressions in the given XPath::Union, or just [union] if it has no sub-expressions. This is an ugly recursive hack, designed to allow splitting up unions into their constituents for the purpose of modifying them individually and re-combining them.

Parameters:

  • union (XPath::Union, XPath::Expression)

    The xpath you want to break down into individual expressions

Since:

  • 0.0.3



142
143
144
145
146
147
148
149
150
# File 'lib/rsel/support.rb', line 142

def xpath_expressions(union)
  if union.respond_to?(:expressions)
    return union.expressions.collect do |expr|
      xpath_expressions(expr)
    end.flatten
  else
    return [union]
  end
end

#xpath_row_containing(texts) ⇒ Object

Return an XPath for any table row containing all strings in texts, within the current context.

Since:

  • 0.1.1



212
213
214
215
216
217
218
# File 'lib/rsel/support.rb', line 212

def xpath_row_containing(texts)
  texts = [texts] if texts.class == String
  conditions = texts.collect do |text|
    "contains(., #{xpath_sanitize(text)})"
  end.join(' and ')
  return "//tr[#{conditions}]"
end

#xpath_sanitize(text) ⇒ Object

Return the given text string in an XPath-safe form, with any single-quotes escaped by using the XPath concat function to combine them with the rest of the text.

Examples:

xpath_sanitize("Bob's")
# => concat('Bob', "'", 's')

Since:

  • 0.1.1



230
231
232
233
234
235
236
237
238
# File 'lib/rsel/support.rb', line 230

def xpath_sanitize(text)
  # If there's nothing to escape, just wrap text in single-quotes
  if !text.include?("'")
    return "'#{text}'"
  else
    result = text.gsub(/'/, %{', "'", '})
    return "concat('#{result}')"
  end
end