rsel 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/.gitignore +1 -0
  2. data/Rakefile +22 -8
  3. data/docs/development.md +1 -0
  4. data/docs/history.md +11 -1
  5. data/docs/index.md +1 -0
  6. data/docs/scoping.md +1 -1
  7. data/docs/studying.md +76 -0
  8. data/lib/rsel/selenium_test.rb +505 -90
  9. data/lib/rsel/study_html.rb +198 -0
  10. data/lib/rsel/support.rb +105 -4
  11. data/rsel.gemspec +1 -1
  12. data/spec/spec_helper.rb +4 -22
  13. data/spec/st_alerts.rb +48 -0
  14. data/spec/st_browser_spec.rb +58 -0
  15. data/spec/st_buttons_spec.rb +95 -0
  16. data/spec/st_checkboxes_spec.rb +235 -0
  17. data/spec/st_conditionals_spec.rb +180 -0
  18. data/spec/st_dropdowns_spec.rb +140 -0
  19. data/spec/st_field_equals_among_spec.rb +48 -0
  20. data/spec/st_fields_equal_among_spec.rb +74 -0
  21. data/spec/st_fields_equal_spec.rb +90 -0
  22. data/spec/st_fields_spec.rb +167 -0
  23. data/spec/st_initialization_spec.rb +33 -0
  24. data/spec/st_links_spec.rb +84 -0
  25. data/spec/st_method_missing_spec.rb +59 -0
  26. data/spec/st_navigation_spec.rb +56 -0
  27. data/spec/st_radiobuttons_spec.rb +123 -0
  28. data/spec/st_respond_to_spec.rb +16 -0
  29. data/spec/st_scenario_spec.rb +26 -0
  30. data/spec/st_set_field_among_spec.rb +45 -0
  31. data/spec/st_set_field_spec.rb +842 -0
  32. data/spec/st_set_fields_among_spec.rb +74 -0
  33. data/spec/st_set_fields_spec.rb +97 -0
  34. data/spec/st_spec_helper.rb +43 -0
  35. data/spec/st_stop_on_failure_spec.rb +199 -0
  36. data/spec/st_tables_spec.rb +42 -0
  37. data/spec/st_temporal_visibility_spec.rb +122 -0
  38. data/spec/st_visibility_spec.rb +125 -0
  39. data/spec/st_waiting_spec.rb +37 -0
  40. data/spec/study_html_spec.rb +310 -0
  41. data/spec/support_spec.rb +163 -13
  42. data/test/server/README.txt +3 -0
  43. data/test/views/alert.erb +15 -0
  44. data/test/views/form.erb +6 -1
  45. data/test/views/index.erb +2 -0
  46. data/test/views/slowtext.erb +1 -1
  47. metadata +38 -9
  48. data/spec/selenium_test_spec.rb +0 -2656
@@ -0,0 +1,198 @@
1
+ require 'rubygems'
2
+ require 'nokogiri'
3
+ require 'rsel/support'
4
+
5
+ module Rsel
6
+ # Class to study a web page: Parses it with Nokogiri, and allows searching and simplifying Selenium-like expressions.
7
+ class StudyHtml
8
+
9
+ include Support
10
+
11
+ # A large sentinel for @dirties.
12
+ NO_PAGE_LOADED = 1000000
13
+
14
+ def initialize(first_page=nil)
15
+ @sections_kept_clean = []
16
+ if first_page
17
+ study(first_page)
18
+ else
19
+ @studied_page = nil
20
+ # Invariant: @dirties == 0 while @keep_clean is true.
21
+ @keep_clean = false
22
+ # No page is loaded. Set a large sentinel value so it's never tested.
23
+ @dirties = NO_PAGE_LOADED
24
+ end
25
+ end
26
+
27
+ # Load a page to study.
28
+ #
29
+ # @param [String] page
30
+ # Any argument that works for Nokogiri::HTML. Often HTML in a string, or a path to a file.
31
+ # @param [Boolean] keep
32
+ # Sets {#keep_clean} with this argument. Default is false, so study(), by default, turns off keep_clean.
33
+ def study(page, keep=false)
34
+ @sections_kept_clean = []
35
+ begin
36
+ @studied_page = Nokogiri::HTML(page)
37
+ @dirties = 0
38
+ rescue => e
39
+ @keep_clean = false
40
+ @dirties = NO_PAGE_LOADED
41
+ @studied_page = nil
42
+ raise e
43
+ end
44
+ @keep_clean = keep
45
+ end
46
+
47
+ # "Dirty" the studied page, marking one (potential) change since the page was studied.
48
+ # This can be undone: see {#undo_last_dirty}
49
+ # Does nothing if {#keep_clean} has been called with true, which may occur from {#study}
50
+ def dirty
51
+ @dirties += 1 unless @keep_clean
52
+ end
53
+
54
+ # Try to un-{#dirty} the studied page by marking one (potential) change since the page was studied not actually a change.
55
+ # This may or may not be enough to resume using the studied page. Cannot be used preemptively.
56
+ def undo_last_dirty
57
+ @dirties -= 1 unless @dirties <= 0
58
+ end
59
+
60
+ # Try to un-{#dirty} the studied page. Returns true on success or false if there was no page to clean.
61
+ def undo_all_dirties
62
+ if @studied_page != nil
63
+ @dirties = 0
64
+ else
65
+ @keep_clean = false
66
+ @dirties = NO_PAGE_LOADED
67
+ end
68
+ return @dirties == 0
69
+ end
70
+
71
+ # Return whether the studied page is clean and ready for analysis. Not a verb - does not {#undo_all_dirties}.
72
+ def clean?
73
+ return @dirties == 0
74
+ end
75
+ #alias_method :clean, :clean?
76
+
77
+ # Turn on or off maintenance of clean status. True prevents {#dirty} from having any effect.
78
+ # Also cleans all dirties (with {#undo_all_dirties}) if true, or dirties the page (with {#dirty}) if false.
79
+ def keep_clean(switch)
80
+ if switch
81
+ if undo_all_dirties
82
+ @keep_clean = true
83
+ return true
84
+ else
85
+ return false
86
+ end
87
+ else
88
+ @keep_clean = false
89
+ dirty
90
+ return true
91
+ end
92
+ end
93
+
94
+ # Return whether keep_clean is on or not. Useful if you want to start keeping clean and then return to your previous state.
95
+ # Invariant: clean? == true if keeping_clean? == true.
96
+ def keeping_clean?
97
+ return @keep_clean
98
+ end
99
+
100
+ # Store the current keep_clean status, and begin forcing study use until the
101
+ # next {#end_section}.
102
+ # A semi-optional block argument returns the first argument to give to {#study}.
103
+ # It's not required if {#clean?}, but otherwise if it's not present an exception
104
+ # will be thrown.
105
+ def begin_section
106
+ last_keep_clean = @keep_clean
107
+ if clean?
108
+ @keep_clean = true
109
+ else
110
+ # This will erase all prior sections.
111
+ study(yield, true)
112
+ end
113
+ @sections_kept_clean.push(last_keep_clean)
114
+ end
115
+
116
+ # Restore the keep_clean status from before the last {#begin_section}.
117
+ # Also marks the page dirty unless the last keep_clean was true.
118
+ # It's fine to call this more than you call begin_section. It will act just
119
+ # like keep_clean(false) if it runs out of stack parameters.
120
+ def end_section
121
+ # Can't just assign - what if nil is popped?
122
+ if @sections_kept_clean.pop
123
+ @keep_clean = true
124
+ else
125
+ @keep_clean = false
126
+ dirty
127
+ end
128
+ return true
129
+ end
130
+
131
+ # Simplify a Selenium-like locator (xpath or a css path), based on the studied page
132
+ #
133
+ # @param [Boolean] x
134
+ # @param [Boolean] tocss
135
+ # Return a css= path as a last resort? Defaults to true.
136
+ def simplify_locator(locator, tocss=true)
137
+ return locator if @dirties > 0
138
+
139
+ # We need a locator using either a css= or locator= expression.
140
+ if locator[0,4] == 'css='
141
+ studied_node = @studied_page.at_css(locator[4,locator.length])
142
+ # If we're already using a css path, don't bother simplifying it to another css path.
143
+ tocss = false
144
+ elsif locator[0,6] == 'xpath=' || locator[0,2] == '//'
145
+ locator = 'xpath='+locator if locator[0,2] == '//'
146
+ studied_node = @studied_page.at_xpath(locator[6,locator.length])
147
+ else
148
+ # Some other kind of locator. Just return it.
149
+ return locator
150
+ end
151
+ # If the path wasn't found, just return the locator; maybe the browser will
152
+ # have better luck. (Or return a better error message!)
153
+ return locator if studied_node == nil
154
+
155
+ # Now let's try simplified locators. First, id.
156
+ return "id=#{studied_node['id']}" if(studied_node['id'] &&
157
+ @studied_page.at_xpath("//*[@id='#{studied_node['id']}']") == studied_node)
158
+ # Next, name. Same pattern.
159
+ return "name=#{studied_node['name']}" if(studied_node['name'] &&
160
+ @studied_page.at_xpath("//*[@name='#{studied_node['name']}']") == studied_node)
161
+
162
+ # Link, perhaps?
163
+ return "link=#{studied_node.inner_text}" if(studied_node.node_name.downcase == 'a' &&
164
+ @studied_page.at_xpath("//a[text()='#{studied_node.inner_text}']") == studied_node)
165
+
166
+ # Finally, try a CSS path. Make that a simple xpath, since nth-of-type doesn't work. But give up if we were told not to convert to CSS.
167
+ return locator unless tocss
168
+ return "xpath=#{studied_node.path}"
169
+ end
170
+
171
+ # Find a studied node by almost any type of Selenium locator. Returns a Nokogiri::Node, or nil if not found.
172
+ def get_node(locator)
173
+ return nil if @dirties > 0
174
+ case locator
175
+ when /^id=/, /^name=/
176
+ locator = locator.gsub("'","\\\\'").gsub(/([a-z]+)=([^ ]*) */, "[@\\1='\\2']")
177
+ locator = locator.sub(/\]([^ ]+) */, "][@value='\\1']")
178
+ return @studied_page.at_xpath("//*#{locator}")
179
+ when /^link=/
180
+ # Parse the link through loc (which may simplify it to an id or something).
181
+ # Then try get_studied_node again. It should not return to this spot.
182
+ return get_node(loc(locator[5,locator.length], 'link'))
183
+ when /^css=/
184
+ return @studied_page.at_css(locator[4,locator.length])
185
+ when /^xpath=/, /^\/\//
186
+ return @studied_page.at_xpath(locator.sub(/^xpath=/,''))
187
+ when /^dom=/, /^document\./
188
+ # Can't parse dom=
189
+ return nil
190
+ else
191
+ locator = locator.sub(/^id(entifier)?=/,'')
192
+ retval = @studied_page.at_xpath("//*[@id='#{locator}']")
193
+ retval = @studied_page.at_xpath("//*[@name='#{locator}']") unless retval
194
+ return retval
195
+ end
196
+ end
197
+ end
198
+ end
@@ -36,7 +36,11 @@ module Rsel
36
36
  elsif locator =~ /^(id=|name=|dom=|xpath=|link=|css=)/
37
37
  return locator
38
38
  else
39
- return xpath(kind, locator, scope)
39
+ if kind.empty?
40
+ raise ArgumentError, "kind is required for Rsel-style locators"
41
+ else
42
+ return xpath(kind, locator, scope)
43
+ end
40
44
  end
41
45
  end
42
46
 
@@ -171,6 +175,8 @@ module Rsel
171
175
  # Normalize the given hash of name => locator mappings.
172
176
  # Converts all keys to lowercase and calls {#escape_for_hash} on them.
173
177
  #
178
+ # @since 0.1.1
179
+ #
174
180
  def normalize_ids(ids)
175
181
  ids = {} unless ids.is_a? Hash
176
182
  ids.keys.each do |key|
@@ -198,10 +204,11 @@ module Rsel
198
204
  return text.gsub(/<\/?[^>]*>/, '')
199
205
  end
200
206
 
201
- # This module defines helper methods for building XPath expressions
202
- # copied from Kelp::XPaths
203
207
  # Return an XPath for any table row containing all strings in `texts`,
204
208
  # within the current context.
209
+ #
210
+ # @since 0.1.1
211
+ #
205
212
  def xpath_row_containing(texts)
206
213
  texts = [texts] if texts.class == String
207
214
  conditions = texts.collect do |text|
@@ -218,6 +225,8 @@ module Rsel
218
225
  # xpath_sanitize("Bob's")
219
226
  # # => concat('Bob', "'", 's')
220
227
  #
228
+ # @since 0.1.1
229
+ #
221
230
  def xpath_sanitize(text)
222
231
  # If there's nothing to escape, just wrap text in single-quotes
223
232
  if !text.include?("'")
@@ -230,6 +239,7 @@ module Rsel
230
239
 
231
240
  # Convert a string like "yes", "true", "1", etc.
232
241
  # Values currently recognized as true, case-insensitive:
242
+ #
233
243
  # * [empty string]
234
244
  # * 1
235
245
  # * Check
@@ -239,17 +249,22 @@ module Rsel
239
249
  # * Selected
240
250
  # * True
241
251
  # * Yes
252
+ #
253
+ # @since 0.1.1
254
+ #
242
255
  def string_is_true?(s)
243
256
  return /^(?:yes|true|on|(?:check|select)(?:ed)?|1|)$/i === s
244
257
  end
245
258
 
246
259
  # Compare values like Selenium does, with regexpi? and globs.
260
+ #
247
261
  # @param [String] text
248
262
  # A string.
249
- #
250
263
  # @param [String] expected
251
264
  # Another string. This one may have glob:, regexp:, etc.
252
265
  #
266
+ # @since 0.1.1
267
+ #
253
268
  def selenium_compare(text, expected)
254
269
  if expected.sub!(/^regexp:/, '')
255
270
  return /#{expected}/ === text
@@ -263,6 +278,92 @@ module Rsel
263
278
  return File.fnmatch(expected, text)
264
279
  end
265
280
  end
281
+
282
+ # Return `text` with glob markers `*` on each end, unless the text
283
+ # begins with `exact:`, `regexp:`, or `regexpi:`. This effectively
284
+ # allows normal text to match as a "contains" search instead of
285
+ # matching the entire string.
286
+ #
287
+ # @param [String] text
288
+ # Text to globify
289
+ #
290
+ # @since 0.1.2
291
+ #
292
+ def globify(text)
293
+ if /^(exact|regexpi?):/ === text
294
+ return text
295
+ else
296
+ return text.sub(/^(glob:)?\*?/, '*').sub(/\*?$/, '*')
297
+ end
298
+ end
299
+
300
+
301
+ # Ensure that a given block gets a result within a timeout.
302
+ #
303
+ # This executes the given block statement once per second, until it returns
304
+ # a value that evaluates as true (meaning anything other than `false` or
305
+ # `nil`), or until the `seconds` timeout is reached. If the block evaluates
306
+ # as true within the timeout, return the block result. Otherwise, return
307
+ # `nil`.
308
+ #
309
+ # If the block never returns a value other than `false` or `nil`, then
310
+ # return `nil`. If the block raises an exception (*any* exception), that's
311
+ # considered a false result, and the block will be retried until a true
312
+ # result is returned, or the `seconds` timeout is reached.
313
+ #
314
+ # @param [Integer, String] seconds
315
+ # Integer number of seconds to keep retrying the block
316
+ # @param [Block] block
317
+ # Any block of code that might evaluate to a non-false value
318
+ #
319
+ # @return
320
+ # Result of the block if it evaluated true-ish within the timeout, nil
321
+ # if the block always evaluated as false or raised an exception.
322
+ #
323
+ # @since 0.1.2
324
+ #
325
+ # TODO: Return false if the block takes too long to execute (and exceeds
326
+ # the timeout)
327
+ #
328
+ def result_within(seconds, &block)
329
+ (seconds.to_i + 1).times do
330
+ result = yield rescue nil
331
+ return result if result
332
+ sleep 1
333
+ end
334
+ return nil
335
+ end
336
+
337
+
338
+ # Ensure that a given block fails within a timeout.
339
+ #
340
+ # This is a kind of counterpart to {#result_within}
341
+ #
342
+ # @param [Integer, String] seconds
343
+ # Integer number of seconds to keep retrying the block
344
+ # @param [Block] block
345
+ # Any block of code that might evaluate to a false value,
346
+ # or raise an exception, within the timeout.
347
+ #
348
+ # @return [Boolean]
349
+ # true if the block failed (returned false/nil or raised an exception)
350
+ # within the timeout, false if the block never failed within the timeout.
351
+ #
352
+ # @since 0.1.2
353
+ #
354
+ def failed_within(seconds, &block)
355
+ (seconds.to_i + 1).times do
356
+ begin
357
+ result = yield
358
+ rescue
359
+ return true
360
+ else
361
+ return true if !result
362
+ end
363
+ sleep 1
364
+ end
365
+ return false
366
+ end
266
367
  end
267
368
  end
268
369
 
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "rsel"
3
- s.version = "0.1.1"
3
+ s.version = "0.1.2"
4
4
  s.summary = "Runs Selenium tests from FitNesse"
5
5
  s.description = <<-EOS
6
6
  Rsel provides a Slim fixture for running Selenium tests, with
@@ -1,32 +1,14 @@
1
+ # This file includes RSpec configuration that is needed for all spec testing.
2
+
1
3
  require 'rspec'
4
+ require 'rspec/autorun' # needed for RSpec 2.6.x
2
5
  require 'rsel'
3
6
  require 'selenium/client'
4
7
 
5
8
  require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test', 'app'))
6
9
 
7
10
  RSpec.configure do |config|
11
+ config.color_enabled = true
8
12
  config.include Rsel
9
13
  config.include Rsel::Support
10
14
  end
11
-
12
-
13
- # Monkeypatch the Selenium::Client::Protocol module,
14
- # to prevent http_post from spewing out a bunch of `puts`es on failure.
15
- # (It's really distracting when running spec tests)
16
- module Selenium
17
- module Client
18
- module Protocol
19
- def http_post(data)
20
- start = Time.now
21
- called_from = caller.detect{|line| line !~ /(selenium-client|vendor|usr\/lib\/ruby|\(eval\))/i}
22
- http = Net::HTTP.new(@host, @port)
23
- http.open_timeout = default_timeout_in_seconds
24
- http.read_timeout = default_timeout_in_seconds
25
- response = http.post('/selenium-server/driver/', data, HTTP_HEADERS)
26
- # <-- Here is where all the puts statements were -->
27
- [ response.body[0..1], response.body ]
28
- end
29
- end
30
- end
31
- end
32
-
@@ -0,0 +1,48 @@
1
+ require 'spec/st_spec_helper'
2
+
3
+ describe 'alerts' do
4
+ describe "#see_alert_within_seconds" do
5
+ before(:each) do
6
+ @st.visit("/alert").should be_true
7
+ end
8
+
9
+ context "passes when" do
10
+ it "sees a generic alert" do
11
+ @st.click("Alert me now")
12
+ @st.see_alert_within_seconds.should be_true
13
+ end
14
+ it "sees a generic alert in time" do
15
+ @st.click("Alert me now")
16
+ @st.see_alert_within_seconds(10).should be_true
17
+ end
18
+ it "sees the specific alert" do
19
+ @st.click("Alert me now")
20
+ @st.see_alert_within_seconds("Ruby alert! Automate your workstations!").should be_true
21
+ end
22
+ it "sees the specific alert in time" do
23
+ @st.click("Alert me soon")
24
+ @st.see_alert_within_seconds("Ruby alert! Automate your workstations!", 10).should be_true
25
+ end
26
+ end
27
+
28
+ context "fails when" do
29
+ it "does not see a generic alert in time" do
30
+ @st.click("Alert me soon")
31
+ @st.see_alert_within_seconds(1).should be_false
32
+ # Clean up the alert, to avoid random errors later.
33
+ #@st.see_alert_within_seconds("Ruby alert! Automate your workstations!", 10).should be_true
34
+ end
35
+ it "does not see the specific alert in time" do
36
+ @st.click("Alert me soon")
37
+ @st.see_alert_within_seconds("Ruby alert! Automate your workstations!", 1).should be_false
38
+ # Clean up the alert, to avoid random errors later.
39
+ #@st.see_alert_within_seconds("Ruby alert! Automate your workstations!", 10).should be_true
40
+ end
41
+ it "sees a different alert message" do
42
+ @st.click("Alert me now")
43
+ @st.see_alert_within_seconds("Ruby alert! Man your workstations!", 10).should be_false
44
+ end
45
+ end
46
+
47
+ end
48
+ end