gless 1.2.0 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
data/Changelog.txt CHANGED
@@ -3,3 +3,7 @@
3
3
  behaviour.
4
4
  - 1.2.0: 10 Jun 2013: Changes by bairyn. Added caching, finding
5
5
  based on parent element, and block validators.
6
+ - 1.3.0: 8 Aug 2013: Detect and work around errors caused by incorrect caching,
7
+ and add support for element arguments, evaluating elements under new parents,
8
+ finding based on children, and the :unique option to ensure only one element
9
+ matches a specification.
@@ -3,14 +3,20 @@
3
3
  module TestGithub
4
4
  class SearchPage < TestGithub::BasePage
5
5
 
6
- element :search_form , :form , :id => 'search_form' , :validator => true
7
- element :search_input , :text_field , :class => 'search-page-input' , :validator => true , :parent => :search_form
6
+ element :search_form , :form , :id => 'search_form' , :validator => true , :child => :search_input
7
+ element :search_input , :text_field , :name => 'q' , :validator => true , :parent => :search_form
8
8
  element :search_button , :button , :text => 'Search' , :validator => true , :parent => :search_form
9
9
 
10
+ element :repository_list , :ul , :class => /\brepolist\b/
11
+ element :repository_link , :link , :click_destination => :RepoPage , :parent => :repository_list , :proc => -> repos, page, name do
12
+ repos.a(:text => name)
13
+ end
14
+ element :repository_elems , :lis , :class => 'public source' , :parent => :repository_list
15
+
10
16
  # Test validator blocks.
11
- add_validator do |browser, session|
12
- browser.url =~ /search/
13
- end
17
+ add_validator do |browser, session|
18
+ browser.url =~ /search/
19
+ end
14
20
 
15
21
  url %r{^:base_url/search}
16
22
  set_entry_url ':base_url/search'
@@ -24,8 +30,7 @@ module TestGithub
24
30
 
25
31
  def goto_repository name
26
32
  @session.log.debug "SearchPage: goto_repository: name: #{name}"
27
- (find_repository name)[:link].click
28
- @session.acceptable_pages = TestGithub::RepoPage
33
+ repository_link(name).click
29
34
  end
30
35
 
31
36
  def find_repository name
@@ -42,7 +47,7 @@ module TestGithub
42
47
  end
43
48
 
44
49
  def repositories
45
- repos = self.lis.select { |li| li.class_name == 'public source' }
50
+ repos = repository_elems
46
51
 
47
52
  @session.log.debug "SearchPage: repositories: repos: #{repos.inspect}"
48
53
 
@@ -119,13 +119,25 @@ module Gless
119
119
  # That's about as complicated as it gets.
120
120
  #
121
121
  # The first two arguments (name and type) are required. The
122
- # rest is a hash. +:validator+, +:click_destination+, +:parent+,
123
- # +:proc+, and +:cache+ (see below) have special meaning.
122
+ # rest is a hash. Six options (see below) have special meaning:
123
+ # +:validator+, +:click_destination+, +:parent+, +:child+
124
+ # +:proc+, +:cache+, and +:unique+ (see below) have special meaning.
124
125
  #
125
126
  # Anything else is taken to be a Watir selector. If no
126
127
  # selector is forthcoming, the name is taken to be the element
127
128
  # id.
128
129
  #
130
+ # The element can also be a collection of elements with the appropriate
131
+ # element type (e.g. +lis+, plural of +li+); however, if it is restricted
132
+ # by non-watir selectors (e.g. with :child), the collection is returned
133
+ # as an +Array+, since watir-webdriver does not support arbitrarily
134
+ # filtering elements from an +ElementCollection+. For
135
+ # reliability, the user can either ensure that the element is only used
136
+ # after being coerced into an array with +.to_a+ to ensure that the
137
+ # collection ends up as an Array in each case (unless the method used
138
+ # is supported by both element collections and arrays), or use a
139
+ # low-level +:proc+ to bypass gless's element finding procedure.
140
+ #
129
141
  # @param [Symbol] basename The name used in the Gless user's code
130
142
  # to refer to this element. This page object ends up with a
131
143
  # method of this name.
@@ -149,18 +161,66 @@ module Gless
149
161
  #
150
162
  # @option opts [Symbol] :parent (nil) A symbol of a parent element
151
163
  # to which matching is restricted.
152
- #
164
+ #
165
+ # @option opts [Symbol, Array<Symbol>, Array<Array<Symbol>>] :child (nil)
166
+ # If present, this restricts element selection to elements that
167
+ # contain the child element. The parent of the child element is
168
+ # overridden with the element being tested; it is therefore safe to
169
+ # set the child element's parent to this one, since it won't result
170
+ # in circular reference. This is useful if an element on a page,
171
+ # that must contain a child element that can be located with its
172
+ # selectors, is used in another way. This can be set to an array to
173
+ # specify multiple children elements. Arguments can be specified in
174
+ # an Array. A :child can point to the Symbol of the child element. To
175
+ # specify multiple children, set :child to an Array of Symbols. To
176
+ # specify arguments to pass to each child, set :child to an Array of
177
+ # Arrays each containing the symbol of the child element and then the
178
+ # arguments passed to it. Examples of each usage:
179
+ #
180
+ # element :games_pane , :div , :class => 'pane' , :child => :tbs_list
181
+ # element :tbs_list , :ul , :class => 'list' , :child => [:tbs_header, :tbs_popular_list]
182
+ # element :tbs_pop_list , :ul , :class => 'list' , :child => [[:tbs_link, 'Battle Game 2'], [:tbs_link, 'Wars']]
183
+ #
184
+ # element :tbs_header , :h3 , :text => 'Turn Based Strategy Games'
185
+ # element :tbs_link , :link , :proc => -> parent, page, name {...}
186
+ #
153
187
  # @option opts [Symbol] :cache (nil) If non-nil, overrides the default
154
188
  # cache setting and determines whether caching is enabled for this
155
189
  # element. If false, a new look-up will be performed each time the
156
190
  # element is accessed, and, if true, a look-up will only be performed
157
191
  # once until the session changes the page.
192
+ #
193
+ # @option opts [Symbol] :unique (false) If true, fail if multiple
194
+ # elements match the element's specification when the element is
195
+ # accessed. Note that this option has no effect on elements with
196
+ # +:proc+s.
158
197
  #
159
198
  # @option opts [Symbol] :proc (nil) If present, specifies a manual,
160
199
  # low-level procedure to return a watir element, which overrides other
161
200
  # selectors. When the watir element is needed, this procedure is
162
- # called with the parent watir element passed as the argument (see
163
- # +:parent+) if it exists, and otherwise the browser.
201
+ # called with the parent watir element passed as the first argument (see
202
+ # +:parent+) if it exists, and otherwise the browser, along with the
203
+ # page as the second argument. Any arguments given to the element
204
+ # at runtime are passed to the procedure after the first, parent,
205
+ # argument. For example, given the definition
206
+ #
207
+ # element :book_list, :ul, :click_destination => :HomePage, :parent => :nonfiction, :proc => -> parent, page, author {...}
208
+ #
209
+ # then whenever +session.book_list "Robyn Dawes"+ is invoked, the procedure will be passed the
210
+ # +:nonfiction+ element, the page for which +:book_list+ was defined,
211
+ # and the string "Robyn Dawes", and should return a Watir element. In
212
+ # the block itself, +parent+ could be used as the root element (the
213
+ # browser with no root element), which can be different if the user
214
+ # decides to restrict the +:book_list+ element under a new parent (e.g.
215
+ # in invoking +@session.bilingual_pane.book_list, in which case parent
216
+ # would be set to the :bilingual_pane element). +page+ refers to the
217
+ # page object in which +:book_list+ is defined, which can be used to
218
+ # refer to other elements and methods on the same page. Any arguments
219
+ # passed to the element are given to the block.
220
+ #
221
+ # Different elements are cached for different
222
+ # arguments. Caching can be disabled for an individual
223
+ # element by passing :cache => false.
164
224
  #
165
225
  # @option opts [Object] ANY All other opts keys are used as
166
226
  # Watir selectors to find the element on the page.
@@ -170,7 +230,7 @@ module Gless
170
230
 
171
231
  # Promote various other things into selectors; do this before
172
232
  # we add in the default below
173
- non_selector_opts = [ :validator, :click_destination, :parent, :cache ]
233
+ non_selector_opts = [ :validator, :click_destination, :parent, :cache, :unique, :child ]
174
234
  if ! opts[:selector]
175
235
  opts[:selector] = {} if ! opts.keys.empty?
176
236
  opts.keys.each do |key|
@@ -190,7 +250,19 @@ module Gless
190
250
  click_destination = opts[:click_destination]
191
251
  validator = opts[:validator]
192
252
  parent = opts[:parent]
253
+ child = opts[:child]
254
+ if child.nil?
255
+ # No child
256
+ child = []
257
+ elsif child.kind_of? Symbol
258
+ # Single child
259
+ child = [[child]]
260
+ elsif (child.kind_of? Array) && (!child.empty?) && (child[0].kind_of? Symbol)
261
+ # Multiple children w/out arguments
262
+ child.map! {|s| [s]}
263
+ end
193
264
  cache = opts[:cache]
265
+ unique = opts[:unique]
194
266
 
195
267
  methname = basename.to_s.tr('-', '_').to_sym
196
268
 
@@ -206,8 +278,8 @@ module Gless
206
278
  # $master_logger.debug "In GenericBasePage, for #{self.name}, element: #{basename} has a special destination when clicked, #{click_destination}"
207
279
  end
208
280
 
209
- define_method methname do
210
- cached_elements[methname] ||= Gless::WrapWatir.new(@browser, @session, self, type, selector, click_destination, parent, cache)
281
+ define_method methname do |*args|
282
+ cached_elements[[methname, *args]] ||= Gless::WrapWatir.new(methname, @browser, @session, self, type, selector, click_destination, parent, child, cache, unique, *args)
211
283
  end
212
284
  end
213
285
 
@@ -398,10 +470,6 @@ module Gless
398
470
  end
399
471
  end
400
472
 
401
- #******************************
402
- # Object Level
403
- #******************************
404
-
405
473
  # @return [Hash] A hash of cached +WrapWatir+ elements indexed by the
406
474
  # symbol name. This hash is cleared whenever the page changes.
407
475
  attr_writer :cached_elements
data/lib/gless/config.rb CHANGED
@@ -27,10 +27,10 @@ module Gless
27
27
  #
28
28
  # @return [Gless::EnvConfig]
29
29
  def initialize
30
- env = (ENV['ENVIRONMENT'] || 'development').to_sym
30
+ @env = (ENV['ENVIRONMENT'] || 'development').to_sym
31
31
 
32
- env_file = "#{@@env_dir}/config/#{env}.yml"
33
- raise "You need to create a configuration file named '#{env}.yml' (generated from the ENVIRONMENT environment variable) under #{@@env_dir}/lib/config" unless File.exists? env_file
32
+ env_file = "#{@@env_dir}/config/#{@env}.yml"
33
+ raise "You need to create a configuration file named '#{@env}.yml' (generated from the ENVIRONMENT environment variable) under #{@@env_dir}/lib/config" unless File.exists? env_file
34
34
 
35
35
  @config = YAML::load_file env_file
36
36
  end
@@ -54,28 +54,62 @@ module Gless
54
54
  # @return [Object] what's left after following each key; could be
55
55
  # basically anything.
56
56
  def get( *args )
57
+ r = get_default nil, *args
58
+ raise "Could not locate '#{args.join '.'}' in YAML config; please ensure that '#{@env}.yml' is up to date." if r.nil?
59
+ r
60
+ end
61
+
62
+ # Optionally get an element from the configuration, otherwise returning the
63
+ # default value.
64
+ #
65
+ # @example
66
+ #
67
+ # @config.get_default false, :global, :cache
68
+ #
69
+ # @return [Object] what's left after following each key, or else the
70
+ # default value.
71
+ def get_default( default, *args )
57
72
  if args.empty?
58
73
  return @config
59
74
  end
60
75
 
61
- return get_sub_tree( @config, *args )
76
+ r = get_sub_tree( @config, *args )
77
+ r.nil? ? default : r
62
78
  end
63
79
 
64
80
  def merge(hash)
65
81
  @config.merge!(hash)
66
82
  end
67
83
 
84
+ # Set an element in the configuration to the given value, passed after all
85
+ # of the indices.
86
+ #
87
+ # @example
88
+ #
89
+ # @config.set :global, :debug, true
90
+ def set(*indices, value)
91
+ set_root @config, value, *indices
92
+ end
93
+
68
94
  private
69
95
 
96
+ def set_root root, value, *indices
97
+ if indices.length > 1
98
+ set_root root[indices[0]], value, *indices[1..-1]
99
+ else
100
+ root[indices[0]] = value
101
+ end
102
+ end
103
+
70
104
  # Recursively does all the heavy lifting for get
71
105
  def get_sub_tree items, elem, *args
72
106
  # Can't use debug logging here, as it maybe isn't turned on yet
73
107
  # puts "In Gless::EnvConfig, get_sub_tree: items: #{items}, elem: #{elem}, args: #{args}"
74
108
 
75
- raise "Could not locate '#{elem}' in YAML config" if items.nil?
109
+ return nil if items.nil?
76
110
 
77
111
  new_items = items[elem.to_sym]
78
- raise "Could not locate '#{elem}' in YAML config" if new_items.nil?
112
+ return nil if new_items.nil?
79
113
 
80
114
  if args.empty?
81
115
  return new_items
data/lib/gless/session.rb CHANGED
@@ -83,6 +83,12 @@ module Gless
83
83
  @config.get(*args)
84
84
  end
85
85
 
86
+ # Just passes through to the Gless::EnvConfig component's +get_default+
87
+ # method.
88
+ def get_config_default(*args)
89
+ @config.get_default(*args)
90
+ end
91
+
86
92
  # Just a shortcut to get to the Gless::Logger object.
87
93
  def log
88
94
  @logger
@@ -174,12 +180,18 @@ module Gless
174
180
  #
175
181
  # @param [Class] pklas The class for the page object that has a
176
182
  # set_entry_url that we are using.
177
- def enter(pklas)
183
+ # @param [Boolean] always (true) Whether to enter the given page even
184
+ def enter(pklas, always = true)
178
185
  log.info "Session: Entering the site directly using the entry point for the #{pklas.name} page class"
179
- @current_page = pklas
180
- @pages[pklas].enter
181
- # Needs to run through our custom acceptable_pages= method
182
- self.acceptable_pages = pklas
186
+
187
+ if always || pklas != @current_page
188
+ @current_page = pklas
189
+ @pages[pklas].enter
190
+ # Needs to run through our custom acceptable_pages= method
191
+ self.acceptable_pages = pklas
192
+ else
193
+ log.debug "Session: Already on page"
194
+ end
183
195
  end
184
196
 
185
197
  # Wait for long-term AJAX-style processing, i.e. watch the page
@@ -21,9 +21,10 @@ module Gless
21
21
  require 'rspec'
22
22
  include RSpec::Matchers
23
23
 
24
- # @return [Gless::WrapWatir] The symbol for the parent of this element,
24
+ # @return [Symbol, Gless::WrapWatir, Watir::Element, Watir::ElementCollection]
25
+ # The symbol for the parent of this element,
25
26
  # restricting the scope of its selectorselement.
26
- attr_accessor :parent
27
+ attr_accessor :gless_parent
27
28
 
28
29
  # Sets up the wrapping.
29
30
  #
@@ -40,6 +41,7 @@ module Gless
40
41
  # The wrapper only considers *visible* matching elements, unless
41
42
  # the selectors include ":invisible => true".
42
43
  #
44
+ # @param [Symbol] name The name of this method; used for debugging.
43
45
  # @param [Gless::Browser] browser
44
46
  # @param [Gless::Session] session
45
47
  # @param [Gless::BasePage] page
@@ -56,13 +58,18 @@ module Gless
56
58
  #
57
59
  # is the selector arguments.
58
60
  # @param [Gless::BasePage, Array<Gless::BasePage>] click_destination Optional. A list of pages that are OK places to end up after we click on this element
59
- # @param [Gless:WrapWatir] parents The symbol for the parent element under which the wrapped element is restricted.
61
+ # @param [Symbol] gless_parent The symbol for the parent element under which the wrapped element is restricted.
62
+ # @param [Array<Array<Object>>] child The list of of the children over which
63
+ # element selection is restricted. Each Array contains the symbol of the
64
+ # element and the arguments passed to it.
60
65
  # @param [Boolean] cache Whether to cache this element. If false,
61
66
  # +find_elem+, unless overridden with its argument, performs a lookup
62
67
  # each time it is invoked; otherwise, the watir element is recorded
63
68
  # and kept until the session changes the page. If nil, the default value
64
69
  # is retrieved from the config.
65
- def initialize(browser, session, page, orig_type, orig_selector_args, click_destination, parent, cache)
70
+ # @param [Boolean] unique Whether to require element matches to be unique.
71
+ def initialize(name, browser, session, page, orig_type, orig_selector_args, click_destination, gless_parent, child, cache, unique, *args)
72
+ @name = name
66
73
  @browser = browser
67
74
  @session = session
68
75
  @page = page
@@ -71,8 +78,11 @@ module Gless
71
78
  @num_retries = 3
72
79
  @wait_time = 30
73
80
  @click_destination = click_destination
74
- @parent = parent
81
+ @gless_parent = gless_parent
82
+ @child = child
75
83
  @cache = cache.nil? ? @session.get_config(:global, :cache) : cache
84
+ @unique = unique
85
+ @args = [*args]
76
86
  end
77
87
 
78
88
  # Finds the element in question; deals with the fact that the
@@ -85,32 +95,56 @@ module Gless
85
95
  # just passes those variables to the Watir browser as normal.
86
96
  #
87
97
  # @param [Boolean] use_cache If not nil, overrides the element's +cache+
88
- # value. If false, the element is re-located; otherwise, if the element
89
- # has already been found, return it.
90
- def find_elem use_cache = nil
98
+ # value. If false, the element is re-located; otherwise, if the element
99
+ # has already been found, return it.
100
+ # @param [Boolean] must_exist (true) Assume the element exists; otherwise,
101
+ # return nil if an exception is thrown while locating the element.
102
+ def find_elem use_cache = nil, must_exist = true
91
103
  use_cache = @cache if use_cache.nil?
92
104
  if use_cache
93
105
  @cached_elem ||= find_elem_directly
94
106
  else
95
107
  @cached_elem = find_elem_directly
96
108
  end
109
+
110
+ if @temporary
111
+ e = @cached_elem
112
+ @cached_elem = nil
113
+ e
114
+ else
115
+ @cached_elem
116
+ end
97
117
  end
98
118
 
99
119
  # Find the element in question, regardless of whether the element has
100
120
  # already been identified. The cache is complete ignored and is not
101
121
  # updated. To update the cache and re-locate the element, use +find_elem
102
122
  # false+
103
- def find_elem_directly
123
+ #
124
+ # @param [Boolean] must_exist (true) Assume the element exists; otherwise,
125
+ # return nil if an exception is thrown while locating the element.
126
+ def find_elem_directly must_exist = true
104
127
  tries=0
105
128
  begin
106
129
  # Do we want to return more than one element?
107
130
  multiples = false
108
131
 
109
- par = parent ? @page.send(parent).find_elem : @browser
132
+ case gless_parent
133
+ when NilClass
134
+ par = @browser
135
+ when Symbol
136
+ par = @page.send(gless_parent).find_elem
137
+ when WrapWatir
138
+ par = gless_parent.find_elem
139
+ when Watir::Element
140
+ par = gless_parent
141
+ when Watir::ElementCollection
142
+ par = gless_parent
143
+ end
110
144
 
111
145
  if @orig_selector_args.has_key? :proc
112
146
  # If it's a Proc, it can handle its own visibility checking
113
- return @orig_selector_args[:proc].call par
147
+ return @orig_selector_args[:proc].call par, @page, *@args
114
148
  else
115
149
  # We want all the relevant elements, so force that if it's
116
150
  # not what was asked for
@@ -125,21 +159,50 @@ module Gless
125
159
  end
126
160
  end
127
161
  @session.log.debug "WrapWatir: find_elem: elements type: #{type}"
128
- elems = par.send(type, @orig_selector_args)
162
+ elems = @child.inject(par.send(type, @orig_selector_args)) do |watir_elems, child_gless_elem_arg|
163
+ watir_elems.select do |watir_elem|
164
+ child_watir_elem = @page.send(child_gless_elem_arg[0], *child_gless_elem_arg[1..-1]).with_parent(watir_elem).find_elem nil, false
165
+ if child_watir_elem.nil?
166
+ false
167
+ else
168
+ if child_watir_elem.kind_of? Watir::Element
169
+ child_watir_elem.exists?
170
+ else
171
+ child_watir_elem.length >= 1
172
+ end
173
+ end
174
+ end
175
+ end
129
176
  end
130
177
 
131
178
  @session.log.debug "WrapWatir: find_elem: elements identified by #{trimmed_selectors.inspect} initial version: #{elems.inspect}"
132
179
 
133
- if elems.nil? or elems.length == 0
180
+ if @unique && elems && elems.length > 1
181
+ @session.log.debug "WrapWatir: find_elem: '#{@name}' is not unique"
182
+ return par.send(@orig_type, :id => /$^ ('#{@name}' is not unique)/)
183
+ elsif elems.nil? || elems.length == 0
134
184
  @session.log.debug "WrapWatir: find_elem: can't find any element identified by #{trimmed_selectors.inspect}"
135
185
  # Generally, watir-webdriver code expects *something*
136
186
  # back, and uses .present? to see if it's really there, so
137
187
  # we get the singleton to satisfy that need.
138
- return par.send(@orig_type, @orig_selector_args)
188
+
189
+ # Check for non-watir selectors, in which case we approximate by
190
+ # creating a watir-selector that never matches.
191
+ if elems.kind_of? Array
192
+ # Rather than proceeding silently without the non-watir selectors,
193
+ # we return an element that is never present, requiring a new
194
+ # element to be returned.
195
+ #
196
+ # Set temporary to avoid caching this element.
197
+ @temporary = true
198
+ return par.send(@orig_type, :id => /$^ ('#{@name}' not found)/)
199
+ else
200
+ return par.send(@orig_type, @orig_selector_args)
201
+ end
139
202
  end
140
203
 
141
204
  # We got something unexpected; just give it back
142
- if ! elems.is_a?(Watir::ElementCollection)
205
+ if ! elems.is_a?(Watir::ElementCollection) && !elems.is_a?(Array)
143
206
  @session.log.debug "WrapWatir: find_elem: elements aren't a collection; returning them"
144
207
  return elems
145
208
  end
@@ -147,12 +210,15 @@ module Gless
147
210
  if multiples
148
211
  # We're OK returning the whole set
149
212
  @session.log.debug "WrapWatir: find_elem: multiples were requested; returning #{elems.inspect}"
213
+ @session.log.info "WrapWatir: find_elem: #{@name} returns an array due to non-watir element filtering; :proc can be used to override behaviour" if elems.kind_of? Array
150
214
  return elems
151
215
  elsif elems.length == 1
152
216
  # It's not a collection; just return it.
153
217
  @session.log.debug "WrapWatir: find_elem: only one item found; returning #{elems[0].inspect}"
154
218
  return elems[0]
155
219
  else
220
+ return nil if elems.length < 1 unless must_exist
221
+
156
222
  unless @orig_selector_args.has_key? :invisible and @orig_selector_args[:invisible]
157
223
  if trimmed_selectors.inspect !~ /password/i
158
224
  @session.log.debug "WrapWatir: find_elem: elements identified by #{trimmed_selectors.inspect} before visibility selection: #{elems.inspect}"
@@ -186,14 +252,23 @@ module Gless
186
252
 
187
253
  if tries < 3
188
254
  @session.log.debug "WrapWatir: find_elem: Retrying after exception."
255
+ tries += 1
189
256
  retry
190
257
  else
191
- @session.log.debug "WrapWatir: find_elem: Giving up after exception."
258
+ @session.log.warn "WrapWatir: find_elem: Giving up after exception."
259
+ raise
192
260
  end
193
- tries += 1
194
261
  end
195
262
  end
196
263
 
264
+ # Retrieve a copy of the gless element with a different parent. With a
265
+ # different parent, the copy may refer to a different element.
266
+ def with_parent(gless_parent)
267
+ copy = clone
268
+ copy.gless_parent = gless_parent
269
+ copy
270
+ end
271
+
197
272
  # Pulls any procs out of the selectors for debugging purposes
198
273
  def trimmed_selectors
199
274
  @orig_selector_args.reject { |k,v| k == :proc }
@@ -201,9 +276,99 @@ module Gless
201
276
 
202
277
  # Passes everything through to the underlying Watir object, but
203
278
  # with logging.
204
- def method_missing(m, *args, &block)
279
+ #
280
+ # If caching is enabled, wraps the element to check for stale element
281
+ # reference errors.
282
+ def wrap_watir_call(m, *args, &block)
205
283
  wrapper_logging(m, args)
206
- find_elem.send(m, *args, &block)
284
+ if ! @cache
285
+ find_elem.send(m, *args, &block)
286
+ else
287
+ # Caching is enabled for this element. We do some complex processing
288
+ # here to appropriately respond to cases in which caching causes
289
+ # issues, in which case we let the user know by emitting a helpful
290
+ # warning and trying again without caching.
291
+
292
+ # catch_stale takes an WatirElement with a block and tries running the
293
+ # block, trying it again with caching disabled if it fails with a
294
+ # +StaleElementReferenceError+. This exists to avoid repetition in the
295
+ # code.
296
+ catch_stale = -> &block do
297
+ begin
298
+ block.call
299
+ rescue Selenium::WebDriver::Error::StaleElementReferenceError => e
300
+ warn_prefix = "#{@name}: "
301
+
302
+ # The method call did something directly but raised the relevant
303
+ # exception. Try without caching enabled.
304
+
305
+ @session.log.warn "#{warn_prefix}caught a StaleElementReferenceError; trying without caching..."
306
+
307
+ begin
308
+ r = block.call false
309
+ @session.log.warn "#{warn_prefix}the call succeeded without caching. Please add \":cache => false\" to prevent this from occurring; disabling caching for this elem."
310
+ @cache = false
311
+ r
312
+ rescue Selenium::WebDriver::Error::StaleElementReferenceError => e
313
+ @session.log.error "#{warn_prefix}disabled caching but caught the same type of exception; failing."
314
+ raise
315
+ end
316
+ end
317
+ end
318
+
319
+ # Each method call might do something directly to an element, or it
320
+ # might return a child element. In case of the former, we catch
321
+ # +StaleElementReferenceError+s. In the case of the latter, wrap the
322
+ # element.
323
+ wrap_object = -> wrap_meth, *wrap_args, wrap_block, &gen_elem do
324
+ catch_stale.call do |use_cache|
325
+ r = gen_elem.call(use_cache).send(wrap_meth, *wrap_args, &wrap_block)
326
+
327
+ # The call itself succeeded, whether we tried disabling caching or
328
+ # not.
329
+ if ! ((r.kind_of? Watir::Element) || (r.kind_of? Watir::ElementCollection))
330
+ # Safe to return.
331
+ r
332
+ else
333
+ # Wrap the result in a new object that catches stale element
334
+ # reference exceptions and responds appropriately.
335
+ #
336
+ # Using a proxy class as a child of +BasicObject+ would be much
337
+ # simpler, but to avoid problems with code that analyzes the
338
+ # object's class, we have some more work to do.
339
+ #
340
+ # Override each instance method with a wrapper that checks for
341
+ # stale element reference errors, and then similarly override
342
+ # +method_missing+.
343
+ wrapped = r.clone
344
+ wrapped.class.instance_methods.each do |inst_meth|
345
+ if inst_meth != :define_singleton_method
346
+ wrapped.define_singleton_method inst_meth do |*inst_args, &inst_block|
347
+ wrap_object.call(inst_meth, *inst_args, inst_block) {|use_cache| gen_elem.call(use_cache).send(wrap_meth, *wrap_args, &wrap_block)}
348
+ end
349
+ end
350
+ end
351
+ wrapped.define_singleton_method :method_missing do |inst_meth, *inst_args, &inst_block|
352
+ wrap_object.call(inst_meth, *inst_args, inst_block) {|use_cache| gen_elem.call(use_cache).send(wrap_meth, *wrap_args, &wrap_block)}
353
+ end
354
+ wrapped
355
+ end
356
+ end
357
+ end
358
+
359
+ wrap_object.call(m, *args, block) {|use_cache| find_elem use_cache}
360
+ end
361
+ end
362
+
363
+ # Passes everything through to the underlying Watir object, first checking
364
+ # whether the method call is the name for another element on the same page,
365
+ # in which case the second element is re-evaluated with a different parent.
366
+ def method_missing(m, *args, &block)
367
+ if @page.class.elements.member? m
368
+ @page.send(m, *args, &block).with_parent self
369
+ else
370
+ wrap_watir_call(m, *args, &block)
371
+ end
207
372
  end
208
373
 
209
374
  # Used to log all pass through behaviours. In debug mode,
@@ -221,7 +386,7 @@ module Gless
221
386
 
222
387
  @session.log.debug "WrapWatir: Calling #{m} with arguments #{args.inspect} on a #{elem.class.name} element identified by: #{trimmed_selectors.inspect}"
223
388
 
224
- if elem.present? && elem.class.name == 'Watir::HTMLElement'
389
+ if (elem.respond_to? :present?) && elem.present? && elem.class.name == 'Watir::HTMLElement'
225
390
  @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}"
226
391
  end
227
392
  end
@@ -264,7 +429,7 @@ module Gless
264
429
  wrapper_logging('click', nil)
265
430
  @session.log.debug "WrapWatir: click: Calling click on a #{elem.class.name} element identified by: #{trimmed_selectors.inspect}"
266
431
  if elem.exists?
267
- elem.click
432
+ wrap_watir_call :click
268
433
  end
269
434
  if block_given?
270
435
  yield
@@ -277,7 +442,7 @@ module Gless
277
442
  else
278
443
  wrapper_logging('click', nil)
279
444
  @session.log.debug "WrapWatir: click: Calling click on a #{elem.class.name} element identified by: #{trimmed_selectors.inspect}"
280
- elem.click
445
+ wrap_watir_call :click
281
446
  end
282
447
  end
283
448
 
@@ -309,21 +474,21 @@ module Gless
309
474
  if elem.class.name == 'Watir::TextField'
310
475
  set_text = args[0]
311
476
  @session.log.debug "WrapWatir: set: setting text on #{elem.inspect}/#{elem.html} to #{set_text}"
312
- elem.set(set_text)
477
+ wrap_watir_call :set, set_text
313
478
 
314
479
  @num_retries.times do |x|
315
480
  @session.log.debug "WrapWatir: Checking that text entry worked"
316
- if elem.value == set_text
481
+ if wrap_watir_call(:value) == set_text
317
482
  break
318
483
  else
319
484
  @session.log.debug "WrapWatir: It did not; sleeping for #{@wait_time} seconds"
320
485
  sleep @wait_time
321
486
  @session.log.debug "WrapWatir: Retrying."
322
487
  wrapper_logging('set', set_text)
323
- elem.set(set_text)
488
+ wrap_watir_call :set, set_text
324
489
  end
325
490
  end
326
- elem.value.to_s.should == set_text.to_s
491
+ wrap_watir_call(:value).to_s.should == set_text.to_s
327
492
  @session.log.debug "WrapWatir: The text entry worked"
328
493
 
329
494
  return self
@@ -331,27 +496,27 @@ module Gless
331
496
  # Double-check radio buttons
332
497
  elsif elem.class.name == 'Watir::Radio'
333
498
  wrapper_logging('set', [])
334
- elem.set
499
+ wrap_watir_call :set
335
500
 
336
501
  @num_retries.times do |x|
337
502
  @session.log.debug "WrapWatir: Checking that the radio selection worked"
338
- if elem.set? == true
503
+ if wrap_watir_call(:set?) == true
339
504
  break
340
505
  else
341
506
  @session.log.debug "WrapWatir: It did not; sleeping for #{@wait_time} seconds"
342
507
  sleep @wait_time
343
508
  @session.log.debug "WrapWatir: Retrying."
344
509
  wrapper_logging('set', [])
345
- elem.set
510
+ wrap_watir_call :set
346
511
  end
347
512
  end
348
- elem.set?.should be_true
513
+ wrap_watir_call(:set?).should be_true
349
514
  @session.log.debug "WrapWatir: The radio set worked"
350
515
 
351
516
  return self
352
517
 
353
518
  else
354
- elem.set(*args)
519
+ wrap_watir_call(:set, *args)
355
520
  end
356
521
  end
357
522
  end
data/lib/gless.rb CHANGED
@@ -6,10 +6,15 @@
6
6
  # project.
7
7
  module Gless
8
8
  # The current version number.
9
- VERSION = '1.2.0'
9
+ VERSION = '1.3.1'
10
10
 
11
11
  # Sets up the config, logger and browser instances, the ordering
12
- # of which is slightly tricky.
12
+ # of which is slightly tricky. If a block is given, the config, after being
13
+ # initialized from the config file, is passed to the block, which should
14
+ # return the new, updated config.
15
+ #
16
+ # @yield [config] The config loaded from the development file. The
17
+ # optional block should return an updated config if given.
13
18
  #
14
19
  # @return [Gless::Logger, Gless::EnvConfig, Gless::Browser] logger, config, browser (in that order)
15
20
  def self.setup( tag )
@@ -17,6 +22,7 @@ module Gless
17
22
 
18
23
  # Create the config reading/storage object
19
24
  config = Gless::EnvConfig.new( )
25
+ config = yield config if block_given?
20
26
 
21
27
  # Get the whole backtrace, please.
22
28
  if config.get :global, :debug
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.2.0
4
+ version: 1.3.1
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-06-10 00:00:00.000000000 Z
12
+ date: 2013-08-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec