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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 545addc7d4b910134d2bbb7a29b6ec990cc190e5
4
- data.tar.gz: 747b5268236901cc5cdf3bfadfa50ff2e9b6cd80
3
+ metadata.gz: 79847ab6fa6a51c16ec5127b80cc6e42035abfde
4
+ data.tar.gz: d20bbebe4ef811ccf92b75e15a3231957fd37fe1
5
5
  SHA512:
6
- metadata.gz: ccff7683bc850c05d0e5f055fa39e92f4bee5f489e43cd1a01a97a90a931802ea493c5f9baa666bf89728f4c0f0a8e35ee281cd8eca869f7482cff47195ba289
7
- data.tar.gz: eafb28c8a5e4ef4e3e450c4dc624064fcf1ecfee5ee7903dedade84853d71155c34687051ee08ecbb5027894c987e244fa102ec89368e9997bd95d5c2fcf7cd4
6
+ metadata.gz: 38aa636f72ece06a5c8722085e898736bc57db7b362c2f54752ec9878a585d8e9d10e6dddd090457ce0778e0909704a33b3e3daa654070345a13092845aa5feb
7
+ data.tar.gz: e75ab259fc8c46c28c1416622af05f6e65081e4f647e9a481c41463c30ae27a9c0267cc834c4e45212aa75b6eaceee5c40cdaaaaed7518ec1bb29d2ba35f013e
@@ -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/page_polling'
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
- seconds_to_wait ||= default_max_wait_time
40
- start_time = Time.now
36
+ def wait_until(seconds_to_wait = nil, error_message = nil, &block)
37
+ @wait_until ||= 0
38
+ @wait_until += 1
41
39
 
42
- until result = yield
43
- delay = seconds_to_wait - (Time.now - start_time)
40
+ result = nil
44
41
 
45
- if delay <= 0
46
- raise WaitTimeoutError, error_message || "Timed out waiting for condition"
47
- end
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
- sleep(0.05)
50
- raise FrozenInTime, "Time appears to be frozen" if Time.now == start_time
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
- subclass.class_eval do
10
- class << self
11
- def element_attributes
12
- @element_attributes ||= {}
13
- end
14
- end
15
- end
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
- poll_until do
10
+ AePageObjects.wait_until do
15
11
  @query.conditions.each do |document_condition|
16
- begin
17
- if document = @strategy.load_document_with_condition(document_condition)
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
- if @locator
99
- locator = eval_locator(@locator)
100
- if ! locator.empty?
101
- return parent.node.find(*locator)
102
- end
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
- poll_until(timeout) do
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 ElementExpectationError < Error
14
- end
15
-
16
- class ElementNotVisible < ElementExpectationError
13
+ class PathNotResolvable < Error
17
14
  end
18
15
 
19
- class ElementNotHidden < ElementExpectationError
16
+ class DocumentLoadError < Error
20
17
  end
21
18
 
22
- class ElementNotPresent < ElementExpectationError
19
+ class CastError < Error
23
20
  end
24
21
 
25
- class ElementNotAbsent < ElementExpectationError
22
+ class WindowNotFound < Error
26
23
  end
27
24
 
28
- class PathNotResolvable < Error
25
+ class WaitTimeoutError < Error
29
26
  end
30
27
 
31
- class DocumentLoadError < Error
28
+ class ElementNotVisible < WaitTimeoutError
32
29
  end
33
30
 
34
- class CastError < Error
31
+ class ElementNotHidden < WaitTimeoutError
35
32
  end
36
33
 
37
- class WindowNotFound < Error
34
+ class ElementNotPresent < WaitTimeoutError
38
35
  end
39
36
 
40
- class WaitTimeoutError < Error
37
+ class ElementNotAbsent < WaitTimeoutError
41
38
  end
42
39
 
43
40
  class FrozenInTime < Error
@@ -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 = [:find, :all, :value, :set, :text, :visible?]
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
- if locator = loaded_locator
92
- find(*eval_locator(locator))
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
@@ -25,7 +25,7 @@ module AePageObjects
25
25
 
26
26
  def condition_matches?(document, condition)
27
27
  condition.match?(document)
28
- rescue ElementExpectationError, LoadingElementFailed
28
+ rescue LoadingElementFailed
29
29
  false
30
30
  end
31
31
  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
@@ -1,3 +1,3 @@
1
1
  module AePageObjects
2
- VERSION = '2.1.0'.freeze
2
+ VERSION = '3.0.0'.freeze
3
3
  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: 2.1.0
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-10 00:00:00.000000000 Z
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/page_polling.rb
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