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 +4 -0
- data/examples/test_github/lib/pages/test_github/search_page.rb +13 -8
- data/lib/gless/base_page.rb +80 -12
- data/lib/gless/config.rb +40 -6
- data/lib/gless/session.rb +17 -5
- data/lib/gless/wrap_watir.rb +196 -31
- data/lib/gless.rb +8 -2
- metadata +2 -2
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 , :
|
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
|
-
|
12
|
-
|
13
|
-
|
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
|
-
(
|
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 =
|
50
|
+
repos = repository_elems
|
46
51
|
|
47
52
|
@session.log.debug "SearchPage: repositories: repos: #{repos.inspect}"
|
48
53
|
|
data/lib/gless/base_page.rb
CHANGED
@@ -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.
|
123
|
-
# +:
|
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
|
-
|
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
|
-
|
109
|
+
return nil if items.nil?
|
76
110
|
|
77
111
|
new_items = items[elem.to_sym]
|
78
|
-
|
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
|
-
|
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
|
-
|
180
|
-
@
|
181
|
-
|
182
|
-
|
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
|
data/lib/gless/wrap_watir.rb
CHANGED
@@ -21,9 +21,10 @@ module Gless
|
|
21
21
|
require 'rspec'
|
22
22
|
include RSpec::Matchers
|
23
23
|
|
24
|
-
# @return [Gless::WrapWatir
|
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 :
|
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 [
|
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
|
-
|
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
|
-
@
|
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
|
-
#
|
89
|
-
#
|
90
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
488
|
+
wrap_watir_call :set, set_text
|
324
489
|
end
|
325
490
|
end
|
326
|
-
|
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
|
-
|
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
|
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
|
-
|
510
|
+
wrap_watir_call :set
|
346
511
|
end
|
347
512
|
end
|
348
|
-
|
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
|
-
|
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.
|
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.
|
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-
|
12
|
+
date: 2013-08-21 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|