ae_page_objects 2.1.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/ae_page_objects.rb +58 -13
- data/lib/ae_page_objects/core/dsl.rb +15 -7
- data/lib/ae_page_objects/document_loader.rb +3 -11
- data/lib/ae_page_objects/element.rb +8 -8
- data/lib/ae_page_objects/element_proxy.rb +1 -9
- data/lib/ae_page_objects/exceptions.rb +9 -12
- data/lib/ae_page_objects/node.rb +19 -7
- data/lib/ae_page_objects/single_window/same_window_loader_strategy.rb +1 -1
- data/lib/ae_page_objects/util/wait_time_manager.rb +20 -0
- data/lib/ae_page_objects/version.rb +1 -1
- metadata +3 -3
- data/lib/ae_page_objects/util/page_polling.rb +0 -53
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 79847ab6fa6a51c16ec5127b80cc6e42035abfde
|
4
|
+
data.tar.gz: d20bbebe4ef811ccf92b75e15a3231957fd37fe1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 38aa636f72ece06a5c8722085e898736bc57db7b362c2f54752ec9878a585d8e9d10e6dddd090457ce0778e0909704a33b3e3daa654070345a13092845aa5feb
|
7
|
+
data.tar.gz: e75ab259fc8c46c28c1416622af05f6e65081e4f647e9a481c41463c30ae27a9c0267cc834c4e45212aa75b6eaceee5c40cdaaaaed7518ec1bb29d2ba35f013e
|
data/lib/ae_page_objects.rb
CHANGED
@@ -3,7 +3,7 @@ require 'capybara/dsl'
|
|
3
3
|
|
4
4
|
require 'ae_page_objects/version'
|
5
5
|
require 'ae_page_objects/exceptions'
|
6
|
-
require 'ae_page_objects/util/
|
6
|
+
require 'ae_page_objects/util/wait_time_manager'
|
7
7
|
|
8
8
|
module AePageObjects
|
9
9
|
autoload :Node, 'ae_page_objects/node'
|
@@ -16,8 +16,6 @@ module AePageObjects
|
|
16
16
|
autoload :Checkbox, 'ae_page_objects/elements/checkbox'
|
17
17
|
|
18
18
|
class << self
|
19
|
-
include PagePolling
|
20
|
-
|
21
19
|
attr_accessor :default_router
|
22
20
|
|
23
21
|
def browser
|
@@ -35,22 +33,69 @@ module AePageObjects
|
|
35
33
|
end
|
36
34
|
end
|
37
35
|
|
38
|
-
def wait_until(seconds_to_wait = nil, error_message = nil)
|
39
|
-
|
40
|
-
|
36
|
+
def wait_until(seconds_to_wait = nil, error_message = nil, &block)
|
37
|
+
@wait_until ||= 0
|
38
|
+
@wait_until += 1
|
41
39
|
|
42
|
-
|
43
|
-
delay = seconds_to_wait - (Time.now - start_time)
|
40
|
+
result = nil
|
44
41
|
|
45
|
-
|
46
|
-
|
47
|
-
|
42
|
+
if @wait_until > 1
|
43
|
+
# We want to ensure that only the top-level wait_until does the waiting error handling,
|
44
|
+
# which allows correct timing and best chance of recovering from an error.
|
45
|
+
result = call_wait_until_block(error_message, &block)
|
46
|
+
else
|
47
|
+
seconds_to_wait ||= default_max_wait_time
|
48
|
+
start_time = Time.now
|
49
|
+
|
50
|
+
# In an effort to avoid flakiness, Capybara waits, rescues errors, reloads nodes, and
|
51
|
+
# retries.
|
52
|
+
#
|
53
|
+
# There are cases when Capybara will rescue an error and either nodes are not
|
54
|
+
# reloadable or the DOM has changed in a such a way that no amount of reloading will
|
55
|
+
# help (but perhaps a retry at the higher level may have a chance of success). This leads
|
56
|
+
# to us needless waiting for a long time just to fail.
|
57
|
+
#
|
58
|
+
# There are also cases when Selenium will take such a long time to respond with an error
|
59
|
+
# that Capybara's timeout will be exceeded and no reloading / retrying will occur. Instead,
|
60
|
+
# Capabara will just raise the error.
|
61
|
+
#
|
62
|
+
# In order to combat the two cases, we start with a lower Capybara wait time and increase
|
63
|
+
# it each iteration. This logic is encapsulated in a little utility class.
|
64
|
+
time_manager = WaitTimeManager.new(1.0, seconds_to_wait)
|
65
|
+
begin
|
66
|
+
result = time_manager.using_wait_time { call_wait_until_block(error_message, &block) }
|
67
|
+
rescue => e
|
68
|
+
errors = Capybara.current_session.driver.invalid_element_errors
|
69
|
+
errors += [Capybara::ElementNotFound]
|
70
|
+
errors += [DocumentLoadError, LoadingElementFailed, LoadingPageFailed]
|
71
|
+
errors += [WaitTimeoutError]
|
72
|
+
raise e unless errors.include?(e.class)
|
73
|
+
|
74
|
+
delay = seconds_to_wait - (Time.now - start_time)
|
75
|
+
|
76
|
+
if delay <= 0
|
77
|
+
# Raising the WaitTimeoutError in the rescue block ensures that Ruby attaches
|
78
|
+
# the original exception as the cause for our WaitTimeoutError.
|
79
|
+
raise WaitTimeoutError, e.message
|
80
|
+
end
|
48
81
|
|
49
|
-
|
50
|
-
|
82
|
+
sleep(0.05)
|
83
|
+
raise FrozenInTime, "Time appears to be frozen" if Time.now == start_time
|
84
|
+
|
85
|
+
retry
|
86
|
+
end
|
51
87
|
end
|
52
88
|
|
53
89
|
result
|
90
|
+
ensure
|
91
|
+
@wait_until -= 1
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def call_wait_until_block(error_message, &block)
|
97
|
+
result = block.call
|
98
|
+
result ? result : raise(WaitTimeoutError, error_message || "Timed out waiting for condition")
|
54
99
|
end
|
55
100
|
|
56
101
|
def default_max_wait_time
|
@@ -6,13 +6,21 @@ module AePageObjects
|
|
6
6
|
include InternalHelpers
|
7
7
|
|
8
8
|
def inherited(subclass)
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
9
|
+
super
|
10
|
+
|
11
|
+
subclass.is_loaded_blocks.push(*is_loaded_blocks)
|
12
|
+
end
|
13
|
+
|
14
|
+
def element_attributes
|
15
|
+
@element_attributes ||= {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def is_loaded_blocks
|
19
|
+
@is_loaded_blocks ||= []
|
20
|
+
end
|
21
|
+
|
22
|
+
def is_loaded(&block)
|
23
|
+
self.is_loaded_blocks << block
|
16
24
|
end
|
17
25
|
|
18
26
|
def element(name, options = {}, &block)
|
@@ -1,9 +1,5 @@
|
|
1
|
-
require 'ae_page_objects/util/page_polling'
|
2
|
-
|
3
1
|
module AePageObjects
|
4
2
|
class DocumentLoader
|
5
|
-
include AePageObjects::PagePolling
|
6
|
-
|
7
3
|
def initialize(query, strategy)
|
8
4
|
@query = query
|
9
5
|
@strategy = strategy
|
@@ -11,14 +7,10 @@ module AePageObjects
|
|
11
7
|
|
12
8
|
def load
|
13
9
|
begin
|
14
|
-
|
10
|
+
AePageObjects.wait_until do
|
15
11
|
@query.conditions.each do |document_condition|
|
16
|
-
|
17
|
-
|
18
|
-
return document
|
19
|
-
end
|
20
|
-
rescue => e
|
21
|
-
raise unless catch_poll_util_error?(e)
|
12
|
+
if document = @strategy.load_document_with_condition(document_condition)
|
13
|
+
return document
|
22
14
|
end
|
23
15
|
end
|
24
16
|
|
@@ -95,15 +95,15 @@ module AePageObjects
|
|
95
95
|
end
|
96
96
|
|
97
97
|
def scoped_node
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
98
|
+
locator = eval_locator(@locator)
|
99
|
+
if locator.empty?
|
100
|
+
parent.node
|
101
|
+
else
|
102
|
+
node = AePageObjects.wait_until { parent.node.first(*locator) }
|
103
|
+
node.allow_reload!
|
104
|
+
node
|
103
105
|
end
|
104
|
-
|
105
|
-
parent.node
|
106
|
-
rescue Capybara::ElementNotFound => e
|
106
|
+
rescue AePageObjects::WaitTimeoutError => e
|
107
107
|
raise LoadingElementFailed, e.message
|
108
108
|
end
|
109
109
|
end
|
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'ae_page_objects/util/page_polling'
|
2
|
-
|
3
1
|
module AePageObjects
|
4
2
|
class ElementProxy
|
5
3
|
# Remove all instance methods so even things like class()
|
@@ -10,8 +8,6 @@ module AePageObjects
|
|
10
8
|
end
|
11
9
|
end
|
12
10
|
|
13
|
-
include AePageObjects::PagePolling
|
14
|
-
|
15
11
|
def initialize(element_class, *args)
|
16
12
|
@element_class = element_class
|
17
13
|
@args = args
|
@@ -121,16 +117,12 @@ module AePageObjects
|
|
121
117
|
|
122
118
|
def reload_element
|
123
119
|
@loaded_element = load_element
|
124
|
-
|
125
|
-
true
|
126
120
|
rescue LoadingElementFailed
|
127
121
|
@loaded_element = nil
|
128
|
-
|
129
|
-
true
|
130
122
|
end
|
131
123
|
|
132
124
|
def with_reloaded_element(timeout)
|
133
|
-
|
125
|
+
AePageObjects.wait_until(timeout) do
|
134
126
|
reload_element
|
135
127
|
yield
|
136
128
|
end
|
@@ -10,34 +10,31 @@ module AePageObjects
|
|
10
10
|
class LoadingElementFailed < Error
|
11
11
|
end
|
12
12
|
|
13
|
-
class
|
14
|
-
end
|
15
|
-
|
16
|
-
class ElementNotVisible < ElementExpectationError
|
13
|
+
class PathNotResolvable < Error
|
17
14
|
end
|
18
15
|
|
19
|
-
class
|
16
|
+
class DocumentLoadError < Error
|
20
17
|
end
|
21
18
|
|
22
|
-
class
|
19
|
+
class CastError < Error
|
23
20
|
end
|
24
21
|
|
25
|
-
class
|
22
|
+
class WindowNotFound < Error
|
26
23
|
end
|
27
24
|
|
28
|
-
class
|
25
|
+
class WaitTimeoutError < Error
|
29
26
|
end
|
30
27
|
|
31
|
-
class
|
28
|
+
class ElementNotVisible < WaitTimeoutError
|
32
29
|
end
|
33
30
|
|
34
|
-
class
|
31
|
+
class ElementNotHidden < WaitTimeoutError
|
35
32
|
end
|
36
33
|
|
37
|
-
class
|
34
|
+
class ElementNotPresent < WaitTimeoutError
|
38
35
|
end
|
39
36
|
|
40
|
-
class
|
37
|
+
class ElementNotAbsent < WaitTimeoutError
|
41
38
|
end
|
42
39
|
|
43
40
|
class FrozenInTime < Error
|
data/lib/ae_page_objects/node.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'ae_page_objects/core/dsl'
|
2
|
+
require 'ae_page_objects/element_proxy'
|
2
3
|
|
3
4
|
module AePageObjects
|
4
5
|
class Node
|
@@ -14,6 +15,14 @@ module AePageObjects
|
|
14
15
|
end
|
15
16
|
end
|
16
17
|
|
18
|
+
is_loaded do
|
19
|
+
if locator = loaded_locator
|
20
|
+
node.first(*eval_locator(locator)) != nil
|
21
|
+
else
|
22
|
+
true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
17
26
|
def initialize(capybara_node)
|
18
27
|
@node = capybara_node
|
19
28
|
@stale = false
|
@@ -49,7 +58,7 @@ module AePageObjects
|
|
49
58
|
self.class.current_url_without_params
|
50
59
|
end
|
51
60
|
|
52
|
-
METHODS_TO_DELEGATE_TO_NODE = [:
|
61
|
+
METHODS_TO_DELEGATE_TO_NODE = [:value, :set, :text, :visible?]
|
53
62
|
METHODS_TO_DELEGATE_TO_NODE.each do |m|
|
54
63
|
class_eval <<-RUBY
|
55
64
|
def #{m}(*args, &block)
|
@@ -88,13 +97,16 @@ module AePageObjects
|
|
88
97
|
end
|
89
98
|
|
90
99
|
def ensure_loaded!
|
91
|
-
|
92
|
-
|
93
|
-
end
|
94
|
-
|
95
|
-
self
|
96
|
-
rescue Capybara::ElementNotFound => e
|
100
|
+
AePageObjects.wait_until { is_loaded? }
|
101
|
+
rescue AePageObjects::WaitTimeoutError => e
|
97
102
|
raise LoadingElementFailed, e.message
|
98
103
|
end
|
104
|
+
|
105
|
+
# This should not block and instead attempt to return immediately (e.g. use #all / #first
|
106
|
+
# instead of #find / #has_selector ). Unfortunately, this is difficult to enforce since even
|
107
|
+
# with #all / #first capyabara may wait.
|
108
|
+
def is_loaded?
|
109
|
+
self.class.is_loaded_blocks.all? { |block| self.instance_eval(&block) }
|
110
|
+
end
|
99
111
|
end
|
100
112
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module AePageObjects
|
2
|
+
class WaitTimeManager
|
3
|
+
def initialize(min_time, max_time)
|
4
|
+
@wait_time = min_time
|
5
|
+
@max_time = max_time
|
6
|
+
end
|
7
|
+
|
8
|
+
def using_wait_time
|
9
|
+
start_time = Time.now
|
10
|
+
@wait_time = [@wait_time, @max_time].min
|
11
|
+
Capybara.using_wait_time(@wait_time) do
|
12
|
+
yield
|
13
|
+
end
|
14
|
+
ensure
|
15
|
+
if Time.now - start_time > @wait_time
|
16
|
+
@wait_time *= 2
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ae_page_objects
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Donnie Tognazzini
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-10-
|
11
|
+
date: 2016-10-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: capybara
|
@@ -58,7 +58,7 @@ files:
|
|
58
58
|
- lib/ae_page_objects/single_window/window.rb
|
59
59
|
- lib/ae_page_objects/util/hash_symbolizer.rb
|
60
60
|
- lib/ae_page_objects/util/internal_helpers.rb
|
61
|
-
- lib/ae_page_objects/util/
|
61
|
+
- lib/ae_page_objects/util/wait_time_manager.rb
|
62
62
|
- lib/ae_page_objects/version.rb
|
63
63
|
homepage: http://github.com/appfolio/ae_page_objects
|
64
64
|
licenses:
|
@@ -1,53 +0,0 @@
|
|
1
|
-
module AePageObjects
|
2
|
-
module PagePolling
|
3
|
-
# Quickly polls the block until it returns (as opposed to throwing an exception). Using poll
|
4
|
-
# is a safer alternative to,
|
5
|
-
#
|
6
|
-
# Capybara.using_wait_time(0) do
|
7
|
-
# has_content?('Admin')
|
8
|
-
# end
|
9
|
-
#
|
10
|
-
# where we want to determine whether 'Admin' is on the page (without waiting for it to appear).
|
11
|
-
# This is often used to switch login / functionality of page objects based on the state of the
|
12
|
-
# page.
|
13
|
-
#
|
14
|
-
# With poll, the above patterns becomes,
|
15
|
-
#
|
16
|
-
# AePageObjects.poll do
|
17
|
-
# has_content?('Admin')
|
18
|
-
# end
|
19
|
-
def poll(seconds_to_wait = nil, &block)
|
20
|
-
result = nil
|
21
|
-
poll_until(seconds_to_wait) do
|
22
|
-
result = block.call
|
23
|
-
true
|
24
|
-
end
|
25
|
-
result
|
26
|
-
end
|
27
|
-
|
28
|
-
# Quickly polls the block until it returns something truthy. This is a helper function for
|
29
|
-
# special cases and probably NOT what you want to use. See AePageObjects#wait_until or
|
30
|
-
# #poll.
|
31
|
-
def poll_until(seconds_to_wait = nil, &block)
|
32
|
-
AePageObjects.wait_until(seconds_to_wait) do
|
33
|
-
# Capybara normally catches errors and retries. However, with the wait time of zero,
|
34
|
-
# capybara catches the errors and immediately reraises them. So we have to catch
|
35
|
-
# those errors in the similar fashion to capybara such that we properly can wait the
|
36
|
-
# whole seconds_to_wait.
|
37
|
-
begin
|
38
|
-
Capybara.using_wait_time(0, &block)
|
39
|
-
rescue => e
|
40
|
-
raise unless catch_poll_until_error?(e)
|
41
|
-
nil
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def catch_poll_until_error?(error)
|
47
|
-
types = Capybara.current_session.driver.invalid_element_errors + [Capybara::ElementNotFound]
|
48
|
-
types.any? do |type|
|
49
|
-
error.is_a?(type)
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|