testable 0.3.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +37 -25
- data/.hound.yml +31 -12
- data/.rubocop.yml +4 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +1 -1
- data/Gemfile +3 -1
- data/{LICENSE.txt → LICENSE.md} +2 -2
- data/README.md +36 -17
- data/Rakefile +52 -11
- data/bin/console +2 -2
- data/bin/setup +0 -0
- data/examples/testable-capybara-context.rb +64 -0
- data/examples/testable-capybara-rspec.rb +70 -0
- data/examples/testable-capybara.rb +46 -0
- data/examples/testable-info.rb +65 -0
- data/examples/testable-watir-context.rb +67 -0
- data/examples/testable-watir-datasetter.rb +52 -0
- data/examples/testable-watir-events.rb +44 -0
- data/examples/testable-watir-ready.rb +34 -0
- data/examples/testable-watir-test.rb +80 -0
- data/examples/testable-watir.rb +118 -0
- data/lib/testable.rb +142 -10
- data/lib/testable/attribute.rb +38 -0
- data/lib/testable/capybara/dsl.rb +82 -0
- data/lib/testable/capybara/node.rb +30 -0
- data/lib/testable/capybara/page.rb +29 -0
- data/lib/testable/context.rb +73 -0
- data/lib/testable/deprecator.rb +40 -0
- data/lib/testable/element.rb +162 -31
- data/lib/testable/errors.rb +6 -2
- data/lib/testable/extensions/core_ruby.rb +13 -0
- data/lib/testable/extensions/data_setter.rb +144 -0
- data/lib/testable/extensions/dom_observer.js +58 -4
- data/lib/testable/extensions/dom_observer.rb +73 -0
- data/lib/testable/locator.rb +63 -0
- data/lib/testable/logger.rb +16 -0
- data/lib/testable/page.rb +216 -0
- data/lib/testable/ready.rb +49 -7
- data/lib/testable/situation.rb +9 -28
- data/lib/testable/version.rb +7 -6
- data/testable.gemspec +19 -9
- metadata +90 -23
- data/circle.yml +0 -3
- data/lib/testable/data_setter.rb +0 -51
- data/lib/testable/dom_update.rb +0 -19
- data/lib/testable/factory.rb +0 -27
- data/lib/testable/interface.rb +0 -114
@@ -1,8 +1,35 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
/*
|
2
|
+
This functionality will only work for browsers that support it.
|
3
|
+
See: http://caniuse.com/#feat=mutationobserver
|
4
|
+
*/
|
5
|
+
|
6
|
+
// WebDriver arguments, which are passed to the MutationObserver.
|
7
|
+
var element = arguments[0];
|
8
|
+
var delay = arguments[1] * 1000;
|
4
9
|
var callback = arguments[2];
|
5
10
|
|
11
|
+
/*
|
12
|
+
The two functions below are similar in what they are doing. Both are
|
13
|
+
disconneting the observer. Both are also invoking WebDriver's callback
|
14
|
+
function.
|
15
|
+
|
16
|
+
notStartedUpdating passes true to the callback, which indicates that the
|
17
|
+
DOM has not yet begun updating.
|
18
|
+
|
19
|
+
startedUpdating passes false to the callback, which indicates that the
|
20
|
+
DOM has begun updating.
|
21
|
+
|
22
|
+
The disconnect is important. You only want to be listening (observing) for the
|
23
|
+
period required, removing the listeners when done. Since there be many DOM
|
24
|
+
operations, you want to disconnet when there is interaction with the page by
|
25
|
+
the automated scripts.
|
26
|
+
|
27
|
+
When observing a node for changes, the callback will not be fired until the
|
28
|
+
DOM has finished changing. That is the only granularity that is required for
|
29
|
+
the Cogent implementation. What specific events occurred is not important
|
30
|
+
because the goal is not to conditionally respond to them; rather just to know
|
31
|
+
when the process has completed.
|
32
|
+
*/
|
6
33
|
var notStartedUpdating = function() {
|
7
34
|
return setTimeout(function() {
|
8
35
|
observer.disconnect();
|
@@ -16,7 +43,34 @@ var startedUpdating = function() {
|
|
16
43
|
callback(false);
|
17
44
|
};
|
18
45
|
|
19
|
-
|
46
|
+
/*
|
47
|
+
Mutation Observer
|
48
|
+
The W3C DOM4 specification initially introduced mutation observers as a
|
49
|
+
replacement for the deprecated mutation events.
|
50
|
+
|
51
|
+
The MutationObserver is a JavaScript native object that allows for observing
|
52
|
+
a change on any node-like DOM Element. "Mutation" means the addition or the
|
53
|
+
removal of a node as well as changes to the node's attribute and data.
|
54
|
+
|
55
|
+
The general approach is to create a MutationObserver object with a defined
|
56
|
+
callback function. The function will execute on every mutation observed by
|
57
|
+
the MutationObserver. The MutationObserver must be bound to a target, which
|
58
|
+
for Cogent would mean the element whose context it is being called upon.
|
59
|
+
|
60
|
+
A MutationObserver can be provided with a set of options, which indicate
|
61
|
+
what kind of events should be observed.
|
62
|
+
|
63
|
+
The childList option checks for additions and removals of the target node's
|
64
|
+
child elements, including text nodes. This is basically looking for any
|
65
|
+
nodes added or removed from documentElement.
|
66
|
+
|
67
|
+
The subtree option checks for mutations to the target as well the target's
|
68
|
+
descendants. So that means every child node of documentElement.
|
69
|
+
|
70
|
+
The attribute option checks for mutations to the target's attributes.
|
71
|
+
|
72
|
+
The characterData option checks for mutations to the target's data.
|
73
|
+
*/
|
20
74
|
var observer = new MutationObserver(startedUpdating);
|
21
75
|
var config = { attributes: true, childList: true, characterData: true, subtree: true };
|
22
76
|
observer.observe(element, config);
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Watir
|
2
|
+
class Element
|
3
|
+
OBSERVER_FILE = "/dom_observer.js".freeze
|
4
|
+
DOM_OBSERVER = File.read("#{File.dirname(__FILE__)}#{OBSERVER_FILE}").freeze
|
5
|
+
|
6
|
+
# This method makes a call to `execute_async_script` which means that the
|
7
|
+
# DOM observer script must explicitly signal that it is finished by
|
8
|
+
# invoking a callback. In this case, the callback is nothing more than
|
9
|
+
# a delay. The delay is being used to allow the DOM to be updated before
|
10
|
+
# script actions continue.
|
11
|
+
#
|
12
|
+
# The method returns true if the DOM has been changed within the element
|
13
|
+
# context, while false means that the DOM has not yet finished changing.
|
14
|
+
# Note the wording: "has not finished changing." It's known that the DOM
|
15
|
+
# is changing because the observer has recognized that. So the question
|
16
|
+
# this method is helping to answer is "has it finished?"
|
17
|
+
#
|
18
|
+
# Consider the following element definition:
|
19
|
+
#
|
20
|
+
# p :page_list, id: 'navlist'
|
21
|
+
#
|
22
|
+
# You could then do this:
|
23
|
+
#
|
24
|
+
# page_list.dom_updated?
|
25
|
+
#
|
26
|
+
# That would return true if the DOM content for page_list has finished
|
27
|
+
# updating. If the DOM was in the process of being updated, this would
|
28
|
+
# return false. You could also do this:
|
29
|
+
#
|
30
|
+
# page_list.wait_until(&:dom_updated?).click
|
31
|
+
#
|
32
|
+
# This will use Watir's wait until functionality to wait for the DOM to
|
33
|
+
# be updated within the context of the element. Note that the "&:" is
|
34
|
+
# that the object that `dom_updated?` is being called on (in this case
|
35
|
+
# `page_list`) substitutes the ampersand. You can also structure it like
|
36
|
+
# this:
|
37
|
+
#
|
38
|
+
# page_list.wait_until do |element|
|
39
|
+
# element.dom_updated?
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# The default delay of waiting for the DOM to start updating is 1.1
|
43
|
+
# second. However, you can pass a delay value when you call the method
|
44
|
+
# to set your own value, which can be useful for particular sensitivities
|
45
|
+
# in the application you are testing.
|
46
|
+
def dom_updated?(delay: 1.1)
|
47
|
+
element_call do
|
48
|
+
begin
|
49
|
+
driver.manage.timeouts.script_timeout = delay + 1
|
50
|
+
driver.execute_async_script(DOM_OBSERVER, wd, delay)
|
51
|
+
rescue Selenium::WebDriver::Error::JavascriptError => e
|
52
|
+
# This situation can occur if the script execution has started before
|
53
|
+
# a new page is fully loaded. The specific error being checked for
|
54
|
+
# here is one that occurs when a new page is loaded as that page is
|
55
|
+
# trying to execute a JavaScript function.
|
56
|
+
retry if e.message.include?(
|
57
|
+
'document unloaded while waiting for result'
|
58
|
+
)
|
59
|
+
raise
|
60
|
+
ensure
|
61
|
+
# Note that this setting here means any user-defined timeout would
|
62
|
+
# effectively be overwritten.
|
63
|
+
driver.manage.timeouts.script_timeout = 1
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
alias dom_has_updated? dom_updated?
|
69
|
+
alias dom_has_changed? dom_updated?
|
70
|
+
alias when_dom_updated dom_updated?
|
71
|
+
alias when_dom_changed dom_updated?
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Testable
|
2
|
+
module Element
|
3
|
+
module Locator
|
4
|
+
private
|
5
|
+
|
6
|
+
# This method is what actually calls the browser instance to find
|
7
|
+
# an element. If there is an element definition like this:
|
8
|
+
#
|
9
|
+
# text_field :username, id: 'username'
|
10
|
+
#
|
11
|
+
# This will become the following:
|
12
|
+
#
|
13
|
+
# browser.text_field(id: 'username')
|
14
|
+
#
|
15
|
+
# Note that the `to_subtype` method is called, which allows for the
|
16
|
+
# generic `element` to be expressed as the type of element, as opposed
|
17
|
+
# to `text_field` or `select_list`. For example, an `element` may be
|
18
|
+
# defined like this:
|
19
|
+
#
|
20
|
+
# element :enable, id: 'enableForm'
|
21
|
+
#
|
22
|
+
# Which means it would look like this:
|
23
|
+
#
|
24
|
+
# Watir::HTMLElement:0x1c8c9 selector={:id=>"enableForm"}
|
25
|
+
#
|
26
|
+
# Whereas getting the subtype would give you:
|
27
|
+
#
|
28
|
+
# Watir::CheckBox:0x12f8b elector={element: (webdriver element)}
|
29
|
+
#
|
30
|
+
# Which is what you would get if the element definition were this:
|
31
|
+
#
|
32
|
+
# checkbox :enable, id: 'enableForm'
|
33
|
+
#
|
34
|
+
# Using the subtype does get tricky for scripts that require the
|
35
|
+
# built-in sychronization aspects and wait states of Watir.
|
36
|
+
#
|
37
|
+
# The approach being used in this method is necessary to allow actions
|
38
|
+
# like `set`, which are not available on `element`, even though other
|
39
|
+
# actions, like `click`, are. But if you use `element` for your element
|
40
|
+
# definitions, and your script requires a series of actions where elements
|
41
|
+
# may be delayed in appearing, you'll get an "unable to locate element"
|
42
|
+
# message, along with a Watir::Exception::UnknownObjectException.
|
43
|
+
#
|
44
|
+
# A check is made if an UnknownObjectException occurs due to a ready
|
45
|
+
# validation. That's necessary because a ready validation has to find
|
46
|
+
# an element in order to determine the ready state, but that element
|
47
|
+
# might not be present.
|
48
|
+
def access_element(element, locators, _qualifiers)
|
49
|
+
if element == "element".to_sym
|
50
|
+
@browser.element(locators).to_subtype
|
51
|
+
else
|
52
|
+
@browser.__send__(element, locators)
|
53
|
+
end
|
54
|
+
rescue Watir::Exception::UnknownObjectException
|
55
|
+
return false if caller_locations.any? do |str|
|
56
|
+
str.to_s.match?("ready_validations_pass?")
|
57
|
+
end
|
58
|
+
|
59
|
+
raise
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require "logger"
|
2
|
+
|
3
|
+
module Testable
|
4
|
+
class Logger
|
5
|
+
def create(output = $stdout)
|
6
|
+
logger = ::Logger.new(output)
|
7
|
+
logger.progname = 'Testable'
|
8
|
+
logger.level = :UNKNOWN
|
9
|
+
logger.formatter = proc do |severity, time, progname, msg|
|
10
|
+
"#{time.strftime('%F %T')} - #{severity} - #{progname} - #{msg}\n"
|
11
|
+
end
|
12
|
+
|
13
|
+
logger
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,216 @@
|
|
1
|
+
require "testable/situation"
|
2
|
+
|
3
|
+
module Testable
|
4
|
+
module Pages
|
5
|
+
include Situation
|
6
|
+
|
7
|
+
# This provides a list of the methods that are defined on the page
|
8
|
+
# definition. This is helpful is you need to query the page to see
|
9
|
+
# if an element has been provided since all elements automatically
|
10
|
+
# become methods on a definition instance.
|
11
|
+
def definition_api
|
12
|
+
public_methods(false) - Object.public_methods
|
13
|
+
end
|
14
|
+
|
15
|
+
# The `visit` method provides navigation to a specific page by passing
|
16
|
+
# in the URL. If no URL is passed in, this method will attempt to use
|
17
|
+
# the `url_is` attribute from the interface it is being called on.
|
18
|
+
def visit(url = nil, &block)
|
19
|
+
no_url_provided if url.nil? && url_attribute.nil?
|
20
|
+
@browser.goto(url) unless url.nil?
|
21
|
+
@browser.goto(url_attribute) if url.nil?
|
22
|
+
when_ready(&block) if block_given?
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
alias view visit
|
27
|
+
alias navigate_to visit
|
28
|
+
alias goto visit
|
29
|
+
alias perform visit
|
30
|
+
|
31
|
+
# A call to `url_attribute` returns what the value of the `url_is`
|
32
|
+
# attribute is for the given interface. It's important to note that
|
33
|
+
# this is not grabbing the URL that is displayed in the browser;
|
34
|
+
# rather it's the one declared in the interface definition, if any.
|
35
|
+
def url_attribute
|
36
|
+
self.class.url_attribute
|
37
|
+
end
|
38
|
+
|
39
|
+
# A call to `url` returns the actual URL of the page that is displayed
|
40
|
+
# in the browser.
|
41
|
+
def url
|
42
|
+
@browser.url
|
43
|
+
end
|
44
|
+
|
45
|
+
alias page_url url
|
46
|
+
alias current_url url
|
47
|
+
|
48
|
+
# A call to `url_match_attribute` returns what the value of the
|
49
|
+
# `url_matches` attribute is for the given interface. It's important
|
50
|
+
# to note that the URL matching mechanism is effectively a regular
|
51
|
+
# expression check.
|
52
|
+
def url_match_attribute
|
53
|
+
value = self.class.url_match_attribute
|
54
|
+
return if value.nil?
|
55
|
+
|
56
|
+
value = Regexp.new(value) unless value.is_a?(Regexp)
|
57
|
+
value
|
58
|
+
end
|
59
|
+
|
60
|
+
# A call to `has_correct_url?`returns true or false if the actual URL
|
61
|
+
# found in the browser matches the `url_matches` assertion. This is
|
62
|
+
# important to note. It's not using the `url_is` attribute nor the URL
|
63
|
+
# displayed in the browser. It's using the `url_matches` attribute.
|
64
|
+
def has_correct_url?
|
65
|
+
no_url_match_is_possible if url_attribute.nil? && url_match_attribute.nil?
|
66
|
+
!(url =~ url_match_attribute).nil?
|
67
|
+
end
|
68
|
+
|
69
|
+
alias displayed? has_correct_url?
|
70
|
+
alias loaded? has_correct_url?
|
71
|
+
|
72
|
+
# A call to `title_attribute` returns what the value of the `title_is`
|
73
|
+
# attribute is for the given definition. It's important to note that
|
74
|
+
# this is not grabbing the title that is displayed in the browser;
|
75
|
+
# rather it's the one declared in the interface definition, if any.
|
76
|
+
def title_attribute
|
77
|
+
self.class.title_attribute
|
78
|
+
end
|
79
|
+
|
80
|
+
# A call to `title` returns the actual title of the page that is
|
81
|
+
# displayed in the browser.
|
82
|
+
def title
|
83
|
+
@browser.title
|
84
|
+
end
|
85
|
+
|
86
|
+
alias page_title title
|
87
|
+
|
88
|
+
# A call to `has_correct_title?` returns true or false if the actual
|
89
|
+
# title of the current page in the browser matches the `title_is`
|
90
|
+
# attribute. Notice that this check is done as part of a match rather
|
91
|
+
# than a direct check. This allows for regular expressions to be used.
|
92
|
+
def has_correct_title?
|
93
|
+
no_title_is_provided if title_attribute.nil?
|
94
|
+
!title.match(title_attribute).nil?
|
95
|
+
end
|
96
|
+
|
97
|
+
# A call to `secure?` returns true if the page is secure and false
|
98
|
+
# otherwise. This is a simple check that looks for whether or not the
|
99
|
+
# current URL begins with 'https'.
|
100
|
+
def secure?
|
101
|
+
!url.match(/^https/).nil?
|
102
|
+
end
|
103
|
+
|
104
|
+
# A call to `markup` returns all markup on a page. Generally you don't
|
105
|
+
# just want the entire markup but rather want to parse the output of
|
106
|
+
# the `markup` call.
|
107
|
+
def markup
|
108
|
+
browser.html
|
109
|
+
end
|
110
|
+
|
111
|
+
alias html markup
|
112
|
+
|
113
|
+
# A call to `text` returns all text on a page. Note that this is text
|
114
|
+
# that is taken out of the markup context. It is unlikely you will just
|
115
|
+
# want the entire text but rather want to parse the output of the
|
116
|
+
# `text` call.
|
117
|
+
def text
|
118
|
+
browser.text
|
119
|
+
end
|
120
|
+
|
121
|
+
alias page_text text
|
122
|
+
|
123
|
+
# This method provides a call to the synchronous `execute_script`
|
124
|
+
# action on the browser, passing in JavaScript that you want to have
|
125
|
+
# executed against the current page. For example:
|
126
|
+
#
|
127
|
+
# result = page.run_script("alert('Cogent ran a script.')")
|
128
|
+
#
|
129
|
+
# You can also run full JavaScript snippets.
|
130
|
+
#
|
131
|
+
# script = <<-JS
|
132
|
+
# return arguments[0].innerHTML
|
133
|
+
# JS
|
134
|
+
#
|
135
|
+
# page.run_script(script, page.account)
|
136
|
+
#
|
137
|
+
# Here you pass two arguments to `run_script`. One is the script itself
|
138
|
+
# and the other are some arguments that you want to pass as part of
|
139
|
+
# of the execution. In this case, an element definition (`account`) is
|
140
|
+
# being passed in.
|
141
|
+
def run_script(script, *args)
|
142
|
+
browser.execute_script(script, *args)
|
143
|
+
end
|
144
|
+
|
145
|
+
alias execute_script run_script
|
146
|
+
|
147
|
+
# A call to `screen_width` returns the width of the browser screen as
|
148
|
+
# reported by the browser API, using a JavaScript call to the `screen`
|
149
|
+
# object.
|
150
|
+
def screen_width
|
151
|
+
run_script("return screen.width;")
|
152
|
+
end
|
153
|
+
|
154
|
+
# A call to `screen_height` returns the height of the browser screen as
|
155
|
+
# reported by the browser API, using a JavaScript call to the `screen`
|
156
|
+
# object.
|
157
|
+
def screen_height
|
158
|
+
run_script("return screen.height;")
|
159
|
+
end
|
160
|
+
|
161
|
+
# This method provides a means to maximize the browser window. This
|
162
|
+
# is done by getting the screen width and height via JavaScript calls.
|
163
|
+
def maximize
|
164
|
+
browser.window.resize_to(screen_width, screen_height)
|
165
|
+
end
|
166
|
+
|
167
|
+
# This method provides a call to the browser window to resize that
|
168
|
+
# window to the specified width and height values.
|
169
|
+
def resize(width, height)
|
170
|
+
browser.window.resize_to(width, height)
|
171
|
+
end
|
172
|
+
|
173
|
+
# This method provides a call to the browser window to move the
|
174
|
+
# window to the specified x and y screen coordinates.
|
175
|
+
def move_to(x_coord, y_coord)
|
176
|
+
browser.window.move_to(x_coord, y_coord)
|
177
|
+
end
|
178
|
+
|
179
|
+
alias resize_to resize
|
180
|
+
|
181
|
+
# This method sends a standard "browser refresh" message to the browser.
|
182
|
+
def refresh
|
183
|
+
browser.refresh
|
184
|
+
end
|
185
|
+
|
186
|
+
alias refresh_page refresh
|
187
|
+
|
188
|
+
# A call to `get_cookie` allows you to specify a particular cookie, by
|
189
|
+
# name, and return the information specified in the cookie.
|
190
|
+
def get_cookie(name)
|
191
|
+
browser.cookies.to_a.each do |cookie|
|
192
|
+
return cookie[:value] if cookie[:name] == name
|
193
|
+
end
|
194
|
+
nil
|
195
|
+
end
|
196
|
+
|
197
|
+
# A call to `clear_cookies` removes all the cookies from the current
|
198
|
+
# instance of the browser that is being controlled by WebDriver.
|
199
|
+
def clear_cookies
|
200
|
+
browser.cookies.clear
|
201
|
+
end
|
202
|
+
|
203
|
+
alias remove_cookies clear_cookies
|
204
|
+
|
205
|
+
# A call to `screenshot` saves a screenshot of the current browser
|
206
|
+
# page. Note that this will grab the entire browser page, even portions
|
207
|
+
# of it that are off panel and need to be scrolled to. You can pass in
|
208
|
+
# the path and filename of the image that you want the screenshot
|
209
|
+
# saved to.
|
210
|
+
def screenshot(file)
|
211
|
+
browser.screenshot.save(file)
|
212
|
+
end
|
213
|
+
|
214
|
+
alias save_screenshot screenshot
|
215
|
+
end
|
216
|
+
end
|
data/lib/testable/ready.rb
CHANGED
@@ -4,7 +4,15 @@ module Testable
|
|
4
4
|
module Ready
|
5
5
|
include Situation
|
6
6
|
|
7
|
-
|
7
|
+
# The ReadyAttributes contains methods that can be called directly on the
|
8
|
+
# interface class definition. These are very much like the attributes
|
9
|
+
# that are used for defining aspects of the pages, such as `url_is` or
|
10
|
+
# `title_is`. These attributes are included separately so as to maintain
|
11
|
+
# more modularity.
|
12
|
+
module ReadyAttributes
|
13
|
+
# This method will provide a list of the ready_validations that have
|
14
|
+
# been defined. This list will contain the list in the order that the
|
15
|
+
# validations were defined in.
|
8
16
|
def ready_validations
|
9
17
|
if superclass.respond_to?(:ready_validations)
|
10
18
|
superclass.ready_validations + _ready_validations
|
@@ -13,41 +21,75 @@ module Testable
|
|
13
21
|
end
|
14
22
|
end
|
15
23
|
|
24
|
+
# When this attribute method is specified on an interface, it will
|
25
|
+
# append the validation provided by the block.
|
16
26
|
def page_ready(&block)
|
17
27
|
_ready_validations << block
|
18
28
|
end
|
19
29
|
|
20
30
|
alias page_ready_when page_ready
|
21
31
|
|
32
|
+
private
|
33
|
+
|
22
34
|
def _ready_validations
|
23
35
|
@_ready_validations ||= []
|
24
36
|
end
|
25
37
|
end
|
26
38
|
|
27
|
-
attr_accessor :ready, :ready_error
|
28
|
-
|
29
39
|
def self.included(caller)
|
30
|
-
caller.extend(
|
40
|
+
caller.extend(ReadyAttributes)
|
31
41
|
end
|
32
42
|
|
33
|
-
|
43
|
+
# If a ready validation fails, the message reported by that failure will
|
44
|
+
# be captured in the `ready_error` accessor.
|
45
|
+
attr_accessor :ready, :ready_error
|
46
|
+
|
47
|
+
# The `when_ready` method is called on an instance of an interface. This
|
48
|
+
# executes the provided validation block after the page has been loaded.
|
49
|
+
# The Ready object instance is yielded into the block.
|
50
|
+
#
|
51
|
+
# Calls to the `ready?` method use a poor-man's cache approach. The idea
|
52
|
+
# here being that when a page has confirmed that it is ready, meaning that
|
53
|
+
# no ready validations have failed, that information is stored so that any
|
54
|
+
# subsequent calls to `ready?` do not query the ready validations again.
|
55
|
+
def when_ready(simple_check = false, &_block)
|
34
56
|
already_marked_ready = ready
|
35
57
|
|
36
|
-
|
58
|
+
unless simple_check
|
59
|
+
no_ready_check_possible unless block_given?
|
60
|
+
end
|
37
61
|
|
38
62
|
self.ready = ready?
|
63
|
+
|
39
64
|
not_ready_validation(ready_error || 'NO REASON PROVIDED') unless ready
|
40
|
-
yield self
|
65
|
+
yield self if block_given?
|
41
66
|
ensure
|
42
67
|
self.ready = already_marked_ready
|
43
68
|
end
|
44
69
|
|
70
|
+
def check_if_ready
|
71
|
+
when_ready(true)
|
72
|
+
end
|
73
|
+
|
74
|
+
# The `ready?` method is used to check if the page has been loaded
|
75
|
+
# successfully, which means that none of the ready validations have
|
76
|
+
# indicated failure.
|
77
|
+
#
|
78
|
+
# When `ready?` is called, the blocks that were stored in the above
|
79
|
+
# `ready_validations` array are instance evaluated against the current
|
80
|
+
# page instance.
|
45
81
|
def ready?
|
46
82
|
self.ready_error = nil
|
47
83
|
return true if ready
|
84
|
+
|
48
85
|
ready_validations_pass?
|
49
86
|
end
|
50
87
|
|
88
|
+
private
|
89
|
+
|
90
|
+
# This method checks if the ready validations that have been specified
|
91
|
+
# have passed. If any ready validation fails, no matter if others have
|
92
|
+
# succeeded, this method immediately returns false.
|
51
93
|
def ready_validations_pass?
|
52
94
|
self.class.ready_validations.all? do |validation|
|
53
95
|
passed, message = instance_eval(&validation)
|