gless 1.0.2 → 1.1.2

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/Changelog.txt ADDED
@@ -0,0 +1,3 @@
1
+ - 1.1.0: 29 Jan 2013: By default, "element"s now ignore non-visible
2
+ elements on the page; use { :invisible => true } to get the old
3
+ behaviour.
@@ -3,8 +3,8 @@
3
3
  module TestGithub
4
4
  class SearchPage < TestGithub::BasePage
5
5
 
6
- element :search_input , :text_field , :class => 'text' , :validator => true
7
- element :search_button , :button , :text => 'Search' , :validator => true
6
+ element :search_input , :text_field , :class => 'search-page-input' , :validator => true
7
+ element :search_button , :button , :text => 'Search' , :validator => true
8
8
 
9
9
  url %r{^:base_url/search}
10
10
 
data/lib/gless/config.rb CHANGED
@@ -44,7 +44,8 @@ module Gless
44
44
  end
45
45
 
46
46
  # Get an element from the configuration. Takes an arbitrary
47
- # number of arguments; each is taken to be a hash key.
47
+ # number of arguments; each is taken to be a hash key. With no
48
+ # arguments, returns the whole configuration.
48
49
  #
49
50
  # @example
50
51
  #
@@ -53,11 +54,13 @@ module Gless
53
54
  # @return [Object] what's left after following each key; could be
54
55
  # basically anything.
55
56
  def get( *args )
57
+ if args.empty?
58
+ return @config
59
+ end
60
+
56
61
  return get_sub_tree( @config, *args )
57
62
  end
58
63
 
59
- # Merge arbitrary data back in to the configuration; used to
60
- # overwrite values that were pulled out of files or whatever.
61
64
  def merge(hash)
62
65
  @config.merge!(hash)
63
66
  end
data/lib/gless/session.rb CHANGED
@@ -105,24 +105,21 @@ module Gless
105
105
 
106
106
  log.debug "Session: check if we've changed pages: #{@browser.title}, #{@browser.url}, #{@previous_url}, #{@current_page}, #{@acceptable_pages}"
107
107
 
108
- # Changed URL means we've changed pages. Our current page no
109
- # longer being in the acceptable pages list means we *should*
110
- # have changed pages. So we check both.
111
- if @browser.url == @previous_url && @acceptable_pages.member?( @current_page )
108
+ # Changed URL means we've changed pages, probably by surprise
109
+ # since desired page changes happen in Gless::WrapWatir#click
110
+ if @browser.url == @previous_url
112
111
  log.debug "Session: doesn't look like we've moved."
113
112
  else
114
- # See if we're on one of the acceptable pages; wait until we
115
- # are for "timeout" seconds.
113
+ # See if we're on one of the acceptable pages. We do no
114
+ # significant waiting because Gless::WrapWatir#click should
115
+ # have handeled that.
116
116
  good_page=false
117
117
  new_page=nil
118
- @timeout.times do
119
- if @acceptable_pages.nil?
120
- # If we haven't gone anywhere yet, anything is good
121
- good_page = true
122
- new_page = @pages[@current_page]
123
- break
124
- end
125
-
118
+ if @acceptable_pages.nil?
119
+ # If we haven't gone anywhere yet, anything is good
120
+ good_page = true
121
+ new_page = @pages[@current_page]
122
+ else
126
123
  @acceptable_pages.each do |page|
127
124
  log.debug "Session: Checking our current url, #{@browser.url}, for a match in #{page.name}: #{@pages[page].match_url(@browser.url)}"
128
125
  if @pages[page].match_url(@browser.url)
@@ -130,20 +127,19 @@ module Gless
130
127
  @current_page = page
131
128
  new_page = @pages[page]
132
129
  log.debug "Session: we seem to be on #{page.name} at #{@browser.url}"
133
- break
134
130
  end
135
131
  end
136
-
137
- if good_page
138
- break
139
- end
140
- sleep 1
141
132
  end
142
133
 
143
134
  good_page.should be_true, "Current URL is #{@browser.url}, which doesn't match any of the acceptable pages: #{@acceptable_pages}"
144
135
 
145
- log.debug "Session: checking for arrival at #{new_page.class.name}"
146
- new_page.arrived?.should be_true
136
+ # While this is very thorough, it slows things down quite a
137
+ # bit, and should mostly be covered by
138
+ # Gless::WrapWatir#click ; leaving here in case we decide we
139
+ # need it later.
140
+ #
141
+ # log.debug "Session: checking for arrival at #{new_page.class.name}"
142
+ # new_page.arrived?.should be_true
147
143
 
148
144
  url=@browser.url
149
145
  log.debug "Session: refreshed browser URL: #{url}"
@@ -154,6 +150,8 @@ module Gless
154
150
  @previous_url = url
155
151
  end
156
152
 
153
+ # End of page checking code.
154
+
157
155
  cpage = @pages[@current_page]
158
156
 
159
157
  if m.inspect =~ /(password|login)/i or args.inspect =~ /(password|login)/i
@@ -264,15 +262,18 @@ module Gless
264
262
  sleep opts[:interval]
265
263
  end
266
264
  rescue Exception => e
267
- self.log.debug "Session: long_wait: Had an exception #{e}"
265
+ self.log.warn "Session: long_wait: Had an exception #{e}"
268
266
  if self.get_config :global, :debug
269
267
  self.log.debug "Session: long_wait: Had an exception in debug mode: #{e.inspect}"
270
268
  self.log.debug "Session: long_wait: Had an exception in debug mode: #{e.message}"
271
269
  self.log.debug "Session: long_wait: Had an exception in debug mode: #{e.backtrace.join("\n")}"
272
270
 
273
- self.log.debug "Session: long_wait: Had an exception, and you're in debug mode, so giving you a debugger."
271
+ self.log.debug "Session: long_wait: Had an exception, and you're in debug mode, so giving you a debugger. Use 'continue' to proceed."
274
272
  debugger
275
273
  end
274
+
275
+ self.log.debug "Session: long_wait: Retrying after exception."
276
+ retry
276
277
  end
277
278
 
278
279
  return false
@@ -314,6 +315,97 @@ module Gless
314
315
  end
315
316
  end
316
317
 
318
+ # Does the heavy lifting of moving between pages when an element
319
+ # has a new page destination. Mostly used by Gless::WrapWatir
320
+ #
321
+ # Note that this attempts to click on the button (or do whatever
322
+ # else the passed block does) many times in an attempt to get to
323
+ # the right page. If multiple attempts are a problem, you
324
+ # should circumvent this method; {WrapWatir#click_once} exists
325
+ # for this purpose.
326
+ #
327
+ # @param [Class, Symbol, Array] newpage The page(s) that we
328
+ # could be moving to; same idea as {acceptable_pages=}
329
+ #
330
+ # @yield A required Proc/code block that contains the action to
331
+ # take to attempt to change pages (i.e. clicking on a button
332
+ # or whatever). May be run multiple times, as the whole point
333
+ # here is to keep trying until it works.
334
+ #
335
+ # @return (Boolean, String) Returns both whether it managed to
336
+ # get to the page in question and, if not, what sort of errors
337
+ # were seen.
338
+ def change_pages click_destination
339
+ self.acceptable_pages = click_destination
340
+
341
+ log.debug "Session: change_pages: checking to see if we have changed pages: #{@browser.title}, #{@current_page}, #{@acceptable_pages}"
342
+
343
+ good_page = false
344
+ error_message = ''
345
+ new_page = nil
346
+
347
+ # See if we're on one of the acceptable pages; wait until we
348
+ # are for "timeout" seconds.
349
+ @timeout.times do
350
+ self.log.debug "Session: change_pages: yielding to passed block."
351
+ yield
352
+ self.log.debug "Session: change_pages: done yielding to passed block."
353
+
354
+ if @acceptable_pages.member?( @current_page )
355
+ good_page = true
356
+ break
357
+ else
358
+ if @acceptable_pages.nil?
359
+ # If we haven't gone anywhere yet, anything is good
360
+ log.debug "Session: change_pages: no acceptable pages, so accepting the current page."
361
+ good_page = true
362
+ new_page = @pages[@current_page]
363
+ break
364
+ end
365
+
366
+ url=@browser.url
367
+ log.debug "Session: change_pages: refreshed browser URL: #{url}"
368
+
369
+ @acceptable_pages.each do |page|
370
+ log.debug "Session: change_pages: Checking our current url, #{url}, for a match in #{page.name}: #{@pages[page].match_url(url)}"
371
+ if @pages[page].match_url(url) and @pages[page].arrived? == true
372
+ good_page = true
373
+ @current_page = page
374
+ new_page = @pages[page]
375
+ log.debug "Session: change_pages: we seem to be on #{page.name} at #{url}"
376
+ end
377
+ end
378
+
379
+ if not new_page.match_url(url)
380
+ good_page = false
381
+ error_message = "Current URL is #{url}, which doesn't match that expected for any of the acceptable pages: #{@acceptable_pages}"
382
+ next
383
+ end
384
+
385
+ log.debug "Session: change_pages: checking for arrival at #{new_page.class.name}"
386
+ if not new_page.arrived?
387
+ good_page = false
388
+ error_message = "The current page, at #{url}, doesn't have all of the elements for any of the acceptable pages: #{@acceptable_pages}"
389
+ next
390
+ end
391
+
392
+ if good_page == true
393
+ break
394
+ else
395
+ sleep 1
396
+ end
397
+ end
398
+ end
399
+
400
+ if good_page
401
+ log.info "Session: change_pages: We have successfully moved to page #{new_page.class.name}"
402
+
403
+ @previous_url = url
404
+ end
405
+
406
+ return good_page, error_message
407
+ end
408
+
317
409
  end
318
410
 
319
411
  end
@@ -18,10 +18,24 @@ module Gless
18
18
  # This shouldn't ever need to be used by a user; it's done
19
19
  # automatically by the +element+ class method.
20
20
  class Gless::WrapWatir
21
+ require 'rspec'
21
22
  include RSpec::Matchers
22
23
 
23
24
  # Sets up the wrapping.
24
25
  #
26
+ # As a special case, note that the selectors can include a :proc
27
+ # element, in which case this is taken to be a Proc that takes
28
+ # the browser as an argument. This is used for cases where
29
+ # finding the element has to happen at runtime or is
30
+ # particularily complicated. In this case the rest of the
31
+ # selectors should include notes the element for debugging
32
+ # purposes. An example of such an element:
33
+ #
34
+ # Gless::WrapWatir.new(@browser, @session, :input, { :custom => "the first input under the div for tab #{tab} with the id 'task_name'", :proc => Proc.new { |browser| browser.div( :id => "tabs-#{tab}" ).input( :id => 'task_name' ) } }, false )
35
+ #
36
+ # The wrapper only considers *visible* matching elements, unless
37
+ # the selectors include ":invisible => true".
38
+ #
25
39
  # @param [Gless::Browser] browser
26
40
  # @param [Gless::Session] session
27
41
  # @param [Symbol] orig_type The type of the element; normally
@@ -42,34 +56,139 @@ module Gless
42
56
  @session = session
43
57
  @orig_type = orig_type
44
58
  @orig_selector_args = orig_selector_args
45
- @elem = @browser.send(@orig_type, @orig_selector_args)
46
59
  @num_retries = 3
47
60
  @wait_time = 30
48
61
  @click_destination = click_destination
49
62
  end
50
63
 
64
+ # Finds the element in question; deals with the fact that the
65
+ # selector could actually be a Proc.
66
+ #
67
+ # Has no parameters because it uses @orig_type and
68
+ # @orig_selector_args. If @orig_selector_args has a :proc
69
+ # element, runs that with the browser as an argument, otherwise
70
+ # just passes those variables to the Watir browser as normal.
71
+ def find_elem
72
+ tries=0
73
+ begin
74
+ # Do we want to return more than on element?
75
+ multiples = false
76
+
77
+ if @orig_selector_args.has_key? :proc
78
+ # If it's a Proc, it can handle its own visibility checking
79
+ return @orig_selector_args[:proc].call @browser
80
+ else
81
+ # We want all the relevant elements, so force that if it's
82
+ # not what was asked for
83
+ type = @orig_type.to_s
84
+ if type =~ %r{s$}
85
+ multiples=true
86
+ else
87
+ if Watir::Container.method_defined?(type + 's')
88
+ type = type + 's'
89
+ elsif Watir::Container.method_defined?(type + 'es')
90
+ type = type + 'es'
91
+ end
92
+ end
93
+ @session.log.debug "WrapWatir: find_elem: elements type: #{type}"
94
+ elems = @browser.send(type, @orig_selector_args)
95
+ end
96
+
97
+ @session.log.debug "WrapWatir: find_elem: elements identified by #{trimmed_selectors.inspect} initial version: #{elems.inspect}"
98
+
99
+ if elems.nil? or elems.length == 0
100
+ @session.log.debug "WrapWatir: find_elem: can't find any element identified by #{trimmed_selectors.inspect}"
101
+ # Generally, watir-webdriver code expects *something*
102
+ # back, and uses .present? to see if it's really there, so
103
+ # we get the singleton to satisfy that need.
104
+ return @browser.send(@orig_type, @orig_selector_args)
105
+ end
106
+
107
+ # We got something unexpected; just give it back
108
+ if ! elems.is_a?(Watir::ElementCollection)
109
+ @session.log.debug "WrapWatir: find_elem: elements aren't a collection; returning them"
110
+ return elems
111
+ end
112
+
113
+ if multiples
114
+ # We're OK returning the whole set
115
+ @session.log.debug "WrapWatir: find_elem: multiples were requested; returning #{elems.inspect}"
116
+ return elems
117
+ elsif elems.length == 1
118
+ # It's not a collection; just return it.
119
+ @session.log.debug "WrapWatir: find_elem: only one item found; returning #{elems[0].inspect}"
120
+ return elems[0]
121
+ else
122
+ unless @orig_selector_args.has_key? :invisible and @orig_selector_args[:invisible]
123
+ if trimmed_selectors.inspect !~ /password/i
124
+ @session.log.debug "WrapWatir: find_elem: elements identified by #{trimmed_selectors.inspect} before visibility selection: #{elems.inspect}"
125
+ end
126
+
127
+ # Find only visible elements
128
+ elem = elems.find { |x| x.present? and x.visible? }
129
+
130
+ if elem.nil?
131
+ # If there *are* no visible ones, take what we've got
132
+ elem = elems[0]
133
+ end
134
+
135
+ if trimmed_selectors.inspect !~ /password/i
136
+ @session.log.debug "WrapWatir: find_elem: element identified by #{trimmed_selectors.inspect} after visibility selection: #{elem.inspect}"
137
+ end
138
+
139
+ return elem
140
+ end
141
+ end
142
+ rescue Exception => e
143
+ @session.log.warn "WrapWatir: find_elem: Had an exception #{e}"
144
+ if @session.get_config :global, :debug
145
+ @session.log.debug "WrapWatir: find_elem: Had an exception in debug mode: #{e.inspect}"
146
+ @session.log.debug "WrapWatir: find_elem: Had an exception in debug mode: #{e.message}"
147
+ @session.log.debug "WrapWatir: find_elem: Had an exception in debug mode: #{e.backtrace.join("\n")}"
148
+
149
+ @session.log.debug "WrapWatir: find_elem: Had an exception, and you're in debug mode, so giving you a debugger. Use 'continue' to proceed."
150
+ debugger
151
+ end
152
+
153
+ if tries < 3
154
+ @session.log.debug "WrapWatir: find_elem: Retrying after exception."
155
+ retry
156
+ else
157
+ @session.log.debug "WrapWatir: find_elem: Giving up after exception."
158
+ end
159
+ tries += 1
160
+ end
161
+ end
162
+
163
+ # Pulls any procs out of the selectors for debugging purposes
164
+ def trimmed_selectors
165
+ @orig_selector_args.reject { |k,v| k == :proc }
166
+ end
167
+
51
168
  # Passes everything through to the underlying Watir object, but
52
169
  # with logging.
53
170
  def method_missing(m, *args, &block)
54
171
  wrapper_logging(m, args)
55
- @elem.send(m, *args, &block)
172
+ find_elem.send(m, *args, &block)
56
173
  end
57
174
 
58
175
  # Used to log all pass through behaviours. In debug mode,
59
176
  # displays details about what method was passed through, and the
60
177
  # nature of the element in question.
61
178
  def wrapper_logging(m, args)
62
- if @orig_selector_args.inspect =~ /password/i
179
+ elem = find_elem
180
+
181
+ if trimmed_selectors.inspect =~ /password/i
63
182
  @session.log.debug "WrapWatir: Doing something with passwords, redacted."
64
183
  else
65
184
  if @session.get_config :global, :debug
66
185
  @session.log.add_to_replay_log( @browser, @session )
67
186
  end
68
187
 
69
- @session.log.debug "WrapWatir: Calling #{m} with arguments #{args.inspect} on a #{@elem.class.name} element identified by: #{@orig_selector_args.inspect}"
188
+ @session.log.debug "WrapWatir: Calling #{m} with arguments #{args.inspect} on a #{elem.class.name} element identified by: #{trimmed_selectors.inspect}"
70
189
 
71
- if @elem.present? && @elem.class.name == 'Watir::HTMLElement'
72
- @session.log.warn "FIXME: You have been lazy and said that something is of type 'element'; its actual type is #{@elem.to_subtype.class.name}; the element is identified by #{@orig_selector_args.inspect}"
190
+ if elem.present? && elem.class.name == 'Watir::HTMLElement'
191
+ @session.log.warn "FIXME: You have been lazy and said that something is of type 'element'; its actual type is #{elem.to_subtype.class.name}; the element is identified by #{trimmed_selectors.inspect}"
73
192
  end
74
193
  end
75
194
  end
@@ -77,14 +196,55 @@ module Gless
77
196
  # A wrapper around Watir's click; handles the changing of
78
197
  # acceptable pages (i.e. click_destination processing, see
79
198
  # {Gless::BasePage} and {Gless::Session} for more details).
80
- def click
199
+ #
200
+ # Unconditionally clicks once, without any error handling; if
201
+ # you want to try to execute a page transition no matter what,
202
+ # just use +click+
203
+ def click_once
204
+ elem = find_elem
205
+
81
206
  if @click_destination
82
- @session.log.debug "WrapWatir: A #{@elem.class.name} element identified by: #{@orig_selector_args.inspect} has a special destination when clicked, #{@click_destination}"
207
+ @session.log.debug "WrapWatir: A #{elem.class.name} element identified by: #{trimmed_selectors.inspect} has a special destination when clicked, #{@click_destination}"
83
208
  @session.acceptable_pages = @click_destination
84
209
  end
85
210
  wrapper_logging('click', nil)
86
- @session.log.debug "WrapWatir: Calling click on a #{@elem.class.name} element identified by: #{@orig_selector_args.inspect}"
87
- @elem.click
211
+ @session.log.debug "WrapWatir: Calling click on a #{elem.class.name} element identified by: #{trimmed_selectors.inspect}"
212
+ elem.click
213
+ end
214
+
215
+ # A wrapper around Watir's click; handles the changing of
216
+ # acceptable pages (i.e. click_destination processing, see
217
+ # {Gless::BasePage} and {Gless::Session} for more details).
218
+ #
219
+ # If you've clicked on an element with a click_destination, it
220
+ # then calls {Gless::Session#change_pages} to do the actual page
221
+ # transition. As such, it may actually click several times,
222
+ # it will keep trying until it works; if that's not what you're
223
+ # looking for, use click_once
224
+ def click
225
+ elem = find_elem
226
+
227
+ if @click_destination
228
+ @session.log.debug "WrapWatir: click: A #{elem.class.name} element identified by: #{trimmed_selectors.inspect} has a special destination when clicked, #{@click_destination}"
229
+ change_pages_out, change_pages_message = @session.change_pages( @click_destination ) do
230
+ wrapper_logging('click', nil)
231
+ @session.log.debug "WrapWatir: click: Calling click on a #{elem.class.name} element identified by: #{trimmed_selectors.inspect}"
232
+ if elem.exists?
233
+ elem.click
234
+ end
235
+ if block_given?
236
+ yield
237
+ end
238
+ end
239
+ # If the return value isn't true, use it as the message to
240
+ # print.
241
+ @session.log.debug "WrapWatir: click: change pages results: #{change_pages_out}, #{change_pages_message}"
242
+ change_pages_out.should be_true, change_pages_message
243
+ else
244
+ wrapper_logging('click', nil)
245
+ @session.log.debug "WrapWatir: click: Calling click on a #{elem.class.name} element identified by: #{trimmed_selectors.inspect}"
246
+ elem.click
247
+ end
88
248
  end
89
249
 
90
250
  # Used by +set+, see description there.
@@ -109,55 +269,55 @@ module Gless
109
269
  # such try.
110
270
  def set(*args)
111
271
  wrapper_logging('set', args)
272
+ elem = find_elem
112
273
 
113
274
  # Double-check text fields
114
- if @elem.class.name == 'Watir::TextField'
275
+ if elem.class.name == 'Watir::TextField'
115
276
  set_text = args[0]
116
- @elem.set(set_text)
277
+ @session.log.debug "WrapWatir: set: setting text on #{elem.inspect}/#{elem.html} to #{set_text}"
278
+ elem.set(set_text)
117
279
 
118
280
  @num_retries.times do |x|
119
281
  @session.log.debug "WrapWatir: Checking that text entry worked"
120
- @elem = @browser.send(@orig_type, @orig_selector_args)
121
- if @elem.value == set_text
282
+ if elem.value == set_text
122
283
  break
123
284
  else
124
285
  @session.log.debug "WrapWatir: It did not; sleeping for #{@wait_time} seconds"
125
286
  sleep @wait_time
126
287
  @session.log.debug "WrapWatir: Retrying."
127
288
  wrapper_logging('set', set_text)
128
- @elem.set(set_text)
289
+ elem.set(set_text)
129
290
  end
130
291
  end
131
- @elem.value.should == set_text
292
+ elem.value.to_s.should == set_text.to_s
132
293
  @session.log.debug "WrapWatir: The text entry worked"
133
294
 
134
295
  return self
135
296
 
136
297
  # Double-check radio buttons
137
- elsif @elem.class.name == 'Watir::Radio'
298
+ elsif elem.class.name == 'Watir::Radio'
138
299
  wrapper_logging('set', [])
139
- @elem.set
300
+ elem.set
140
301
 
141
302
  @num_retries.times do |x|
142
303
  @session.log.debug "WrapWatir: Checking that the radio selection worked"
143
- @elem = @browser.send(@orig_type, @orig_selector_args)
144
- if @elem.set? == true
304
+ if elem.set? == true
145
305
  break
146
306
  else
147
307
  @session.log.debug "WrapWatir: It did not; sleeping for #{@wait_time} seconds"
148
308
  sleep @wait_time
149
309
  @session.log.debug "WrapWatir: Retrying."
150
310
  wrapper_logging('set', [])
151
- @elem.set
311
+ elem.set
152
312
  end
153
313
  end
154
- @elem.set?.should be_true
314
+ elem.set?.should be_true
155
315
  @session.log.debug "WrapWatir: The radio set worked"
156
316
 
157
317
  return self
158
318
 
159
319
  else
160
- @elem.set(*args)
320
+ elem.set(*args)
161
321
  end
162
322
  end
163
323
  end
data/lib/gless.rb CHANGED
@@ -6,7 +6,7 @@
6
6
  # project.
7
7
  module Gless
8
8
  # The current version number.
9
- VERSION = '1.0.2'
9
+ VERSION = '1.1.2'
10
10
 
11
11
  # Sets up the config, logger and browser instances, the ordering
12
12
  # of which is slightly tricky.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gless
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.1.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-08 00:00:00.000000000 Z
12
+ date: 2013-02-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
@@ -119,6 +119,7 @@ extra_rdoc_files: []
119
119
  files:
120
120
  - .gitignore
121
121
  - .rvmrc
122
+ - Changelog.txt
122
123
  - README.md
123
124
  - Rakefile
124
125
  - TODO