ae_page_objects 2.1.0 → 3.0.0

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