gless 1.2.0 → 1.3.1

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