testable 0.3.0 → 0.4.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 +5 -5
- data/.gitignore +33 -25
- data/.hound.yml +21 -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 +12 -25
- data/Rakefile +20 -11
- data/bin/console +2 -2
- 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 +67 -0
- data/examples/testable-watir.rb +118 -0
- data/lib/testable/attribute.rb +38 -0
- data/lib/testable/context.rb +72 -0
- data/lib/testable/element.rb +162 -31
- data/lib/testable/errors.rb +6 -2
- data/lib/testable/extensions/data_setter.rb +109 -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/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/lib/testable.rb +37 -10
- data/testable.gemspec +16 -8
- metadata +70 -18
- 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
@@ -0,0 +1,118 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$LOAD_PATH << "./lib"
|
3
|
+
|
4
|
+
require "rspec"
|
5
|
+
# rubocop:disable Style/MixinUsage
|
6
|
+
include RSpec::Matchers
|
7
|
+
# rubocop:enable Style/MixinUsage
|
8
|
+
|
9
|
+
require "testable"
|
10
|
+
|
11
|
+
class Home
|
12
|
+
include Testable
|
13
|
+
|
14
|
+
url_is "https://veilus.herokuapp.com/"
|
15
|
+
url_matches(/heroku/)
|
16
|
+
title_is "Veilus"
|
17
|
+
|
18
|
+
# Elements can be defined with HTML-style names as found in Watir.
|
19
|
+
p :login_form, id: "open", visible: true
|
20
|
+
text_field :username, id: "username"
|
21
|
+
text_field :password
|
22
|
+
button :login, id: "login-button"
|
23
|
+
div :message, class: "notice"
|
24
|
+
|
25
|
+
# Elements can be defined with a generic name.
|
26
|
+
# element :login_form, id: "open", visible: true
|
27
|
+
# element :username, id: "username"
|
28
|
+
# element :password
|
29
|
+
# element :login, id: "login-button"
|
30
|
+
# element :message, class: "notice"
|
31
|
+
end
|
32
|
+
|
33
|
+
# You can pass argument options to the driver:
|
34
|
+
|
35
|
+
# args = ['user-data-dir=~/Library/Application\ Support/Google/Chrome']
|
36
|
+
# Testable.start_browser :chrome, options: {args: args}
|
37
|
+
|
38
|
+
# You can pass switches to the driver:
|
39
|
+
|
40
|
+
# Testable.set_browser :chrome, switches: %w[--ignore-certificate-errors
|
41
|
+
# --disable-popup-blocking
|
42
|
+
# --disable-translate
|
43
|
+
# --disable-notifications
|
44
|
+
# --disable-gpu
|
45
|
+
# --disable-login-screen-apps
|
46
|
+
# ]
|
47
|
+
|
48
|
+
Testable.start_browser :firefox
|
49
|
+
|
50
|
+
page = Home.new
|
51
|
+
|
52
|
+
# You can specify a URL to visit or you can rely on the provided
|
53
|
+
# url_is attribute on the page definition. So you could do this:
|
54
|
+
# page.visit("https://veilus.herokuapp.com/")
|
55
|
+
page.visit
|
56
|
+
|
57
|
+
expect(page.url).to eq(page.url_attribute)
|
58
|
+
expect(page.url).to match(page.url_match_attribute)
|
59
|
+
expect(page.title).to eq(page.title_attribute)
|
60
|
+
|
61
|
+
expect(page.has_correct_url?).to be_truthy
|
62
|
+
expect(page).to have_correct_url
|
63
|
+
|
64
|
+
expect(page.displayed?).to be_truthy
|
65
|
+
expect(page).to be_displayed
|
66
|
+
|
67
|
+
expect(page.has_correct_title?).to be_truthy
|
68
|
+
expect(page).to have_correct_title
|
69
|
+
|
70
|
+
expect(page.secure?).to be_truthy
|
71
|
+
expect(page).to be_secure
|
72
|
+
|
73
|
+
expect(page.html.include?('<article id="index">')).to be_truthy
|
74
|
+
expect(page.text.include?("Running a Local Version")).to be_truthy
|
75
|
+
|
76
|
+
page.login_form.click
|
77
|
+
page.username.set "admin"
|
78
|
+
page.password(id: 'password').set "admin"
|
79
|
+
page.login.click
|
80
|
+
expect(page.message.text).to eq('You are now logged in as admin.')
|
81
|
+
|
82
|
+
page.run_script("alert('Testing');")
|
83
|
+
|
84
|
+
expect(page.browser.alert.exists?).to be_truthy
|
85
|
+
expect(page.browser.alert.text).to eq("Testing")
|
86
|
+
page.browser.alert.ok
|
87
|
+
expect(page.browser.alert.exists?).to be_falsy
|
88
|
+
|
89
|
+
# You have to sometimes go down to Selenium to do certain things with
|
90
|
+
# the browser. Here the browser (which is a Watir Browser) that is part
|
91
|
+
# of the definition (page) is referencing the driver (which is a Selenium
|
92
|
+
# Driver) and is then calling into the `manage` subsystem, which gives
|
93
|
+
# access to the window.
|
94
|
+
page.browser.driver.manage.window.minimize
|
95
|
+
|
96
|
+
# Sleeps are a horrible thing. But they are useful for demonstrations.
|
97
|
+
# In this case, the sleep is there just to let you see that the browser
|
98
|
+
# did minimize before it gets maximized.
|
99
|
+
sleep 2
|
100
|
+
|
101
|
+
page.maximize
|
102
|
+
|
103
|
+
# Another brief sleep just to show that the maximize did fact work.
|
104
|
+
sleep 2
|
105
|
+
|
106
|
+
page.resize_to(640, 480)
|
107
|
+
|
108
|
+
# A sleep to show that the resize occurs.
|
109
|
+
sleep 2
|
110
|
+
|
111
|
+
page.move_to(page.screen_width / 2, page.screen_height / 2)
|
112
|
+
|
113
|
+
# A sleep to show that the move occurs.
|
114
|
+
sleep 2
|
115
|
+
|
116
|
+
page.screenshot("testing.png")
|
117
|
+
|
118
|
+
Testable.quit_browser
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "testable/situation"
|
2
|
+
|
3
|
+
module Testable
|
4
|
+
module Pages
|
5
|
+
module Attribute
|
6
|
+
include Situation
|
7
|
+
|
8
|
+
def url_is(url = nil)
|
9
|
+
url_is_empty if url.nil? && url_attribute.nil?
|
10
|
+
url_is_empty if url.nil? || url.empty?
|
11
|
+
@url = url
|
12
|
+
end
|
13
|
+
|
14
|
+
def url_attribute
|
15
|
+
@url
|
16
|
+
end
|
17
|
+
|
18
|
+
def url_matches(pattern = nil)
|
19
|
+
url_match_is_empty if pattern.nil?
|
20
|
+
url_match_is_empty if pattern.is_a?(String) && pattern.empty?
|
21
|
+
@url_match = pattern
|
22
|
+
end
|
23
|
+
|
24
|
+
def url_match_attribute
|
25
|
+
@url_match
|
26
|
+
end
|
27
|
+
|
28
|
+
def title_is(title = nil)
|
29
|
+
title_is_empty if title.nil? || title.empty?
|
30
|
+
@title = title
|
31
|
+
end
|
32
|
+
|
33
|
+
def title_attribute
|
34
|
+
@title
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Testable
|
2
|
+
module Context
|
3
|
+
# Creates a definition context for actions and establishes the context
|
4
|
+
# for execution. Given an interface definition for a page like this:
|
5
|
+
#
|
6
|
+
# class TestPage
|
7
|
+
# include Testable
|
8
|
+
#
|
9
|
+
# url_is "http://localhost:9292"
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# You can do the following:
|
13
|
+
#
|
14
|
+
# on_visit(TestPage)
|
15
|
+
def on_visit(definition, &block)
|
16
|
+
create_active(definition)
|
17
|
+
@active.visit
|
18
|
+
verify_page(@active)
|
19
|
+
call_block(&block)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Creates a definition context for actions. If an existing context
|
23
|
+
# exists, that context will be re-used. You can use this simply to keep
|
24
|
+
# the context for a script clear. For example, say you have the following
|
25
|
+
# interface definitions for pages:
|
26
|
+
#
|
27
|
+
# class Home
|
28
|
+
# include Testable
|
29
|
+
# url_is "http://localhost:9292"
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# class Navigation
|
33
|
+
# include Testable
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# You could then do this:
|
37
|
+
#
|
38
|
+
# on_visit(Home)
|
39
|
+
# on(Navigation)
|
40
|
+
#
|
41
|
+
# The Home definition needs the url_is attribute in order for the on_view
|
42
|
+
# factory to work. But Navigation does not because the `on` method is not
|
43
|
+
# attempting to visit, simply to reference.
|
44
|
+
def on(definition, &block)
|
45
|
+
create_active(definition)
|
46
|
+
call_block(&block)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# This method is used to provide a means for checking if a page has been
|
52
|
+
# navigated to correctly as part of a context. This is useful because
|
53
|
+
# the context signature should remain highly readable, and checks for
|
54
|
+
# whether a given page has been reached would make the context definition
|
55
|
+
# look sloppy.
|
56
|
+
def verify_page(context)
|
57
|
+
return if context.url_match_attribute.nil?
|
58
|
+
return if context.has_correct_url?
|
59
|
+
|
60
|
+
raise Testable::Errors::PageURLFromFactoryNotVerified
|
61
|
+
end
|
62
|
+
|
63
|
+
def create_active(definition)
|
64
|
+
@active = definition.new unless @active.is_a?(definition)
|
65
|
+
end
|
66
|
+
|
67
|
+
def call_block(&block)
|
68
|
+
yield @active if block
|
69
|
+
@active
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
data/lib/testable/element.rb
CHANGED
@@ -1,14 +1,9 @@
|
|
1
1
|
require "watir"
|
2
|
-
require "testable/situation"
|
3
2
|
|
4
3
|
module Testable
|
5
|
-
include Situation
|
6
|
-
|
7
4
|
module_function
|
8
5
|
|
9
|
-
|
10
|
-
@elements = Watir::Container.instance_methods unless @elements
|
11
|
-
end
|
6
|
+
NATIVE_QUALIFIERS = %i[visible].freeze
|
12
7
|
|
13
8
|
def elements?
|
14
9
|
@elements
|
@@ -18,38 +13,174 @@ module Testable
|
|
18
13
|
@elements.include? method.to_sym
|
19
14
|
end
|
20
15
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
16
|
+
def elements
|
17
|
+
@elements ||= Watir::Container.instance_methods unless @elements
|
18
|
+
end
|
19
|
+
|
20
|
+
module Pages
|
21
|
+
module Element
|
22
|
+
# This iterator goes through the Watir container methods and
|
23
|
+
# provides a method for each so that Watir-based element names
|
24
|
+
# cane be defined on an interface definition, as part of an
|
25
|
+
# element definition.
|
26
|
+
Testable.elements.each do |element|
|
27
|
+
define_method(element) do |*signature, &block|
|
28
|
+
identifier, signature = parse_signature(signature)
|
29
|
+
context = context_from_signature(signature, &block)
|
30
|
+
define_element_accessor(identifier, signature, element, &context)
|
31
|
+
end
|
28
32
|
end
|
29
|
-
end
|
30
33
|
|
31
|
-
|
34
|
+
private
|
35
|
+
|
36
|
+
# A "signature" consists of a full element definition. For example:
|
37
|
+
#
|
38
|
+
# text_field :username, id: 'username'
|
39
|
+
#
|
40
|
+
# The signature of this element definition is:
|
41
|
+
#
|
42
|
+
# [:username, {:id=>"username"}]
|
43
|
+
#
|
44
|
+
# This is the identifier of the element (`username`) and the locator
|
45
|
+
# provided for it. This method separates out the identifier and the
|
46
|
+
# locator.
|
47
|
+
def parse_signature(signature)
|
48
|
+
[signature.shift, signature.shift]
|
49
|
+
end
|
32
50
|
|
33
|
-
|
34
|
-
#
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
51
|
+
# Returns the block or proc that serves as a context for an element
|
52
|
+
# definition. Consider the following element definitions:
|
53
|
+
#
|
54
|
+
# ul :facts, id: 'fact-list'
|
55
|
+
# span :fact, -> { facts.span(class: 'site-item')}
|
56
|
+
#
|
57
|
+
# Here the second element definition provides a proc that contains a
|
58
|
+
# context for another element definition. That leads to the following
|
59
|
+
# construction being sent to the browser:
|
60
|
+
#
|
61
|
+
# @browser.ul(id: 'fact-list').span(class: 'site-item')
|
62
|
+
def context_from_signature(*signature, &block)
|
63
|
+
if block_given?
|
64
|
+
block
|
65
|
+
else
|
66
|
+
context = signature.shift
|
67
|
+
context.is_a?(Proc) && signature.empty? ? context : nil
|
68
|
+
end
|
39
69
|
end
|
40
|
-
end
|
41
70
|
|
42
|
-
|
43
|
-
|
44
|
-
|
71
|
+
# This method provides the means to get the aspects of an accessor
|
72
|
+
# signature. The "aspects" refer to the locator information and any
|
73
|
+
# qualifier information that was provided along with the locator.
|
74
|
+
# This is important because the qualifier is not used to locate an
|
75
|
+
# element but rather to put conditions on how the state of the
|
76
|
+
# element is checked as it is being looked for.
|
77
|
+
#
|
78
|
+
# Note that "qualifiers" here refers to Watir boolean methods.
|
79
|
+
def accessor_aspects(element, *signature)
|
80
|
+
identifier = signature.shift
|
81
|
+
locator_args = {}
|
82
|
+
qualifier_args = {}
|
83
|
+
gather_aspects(identifier, element, locator_args, qualifier_args)
|
84
|
+
[locator_args, qualifier_args]
|
85
|
+
end
|
45
86
|
|
46
|
-
|
47
|
-
|
87
|
+
# This method is used to separate the two aspects of an accessor --
|
88
|
+
# the locators and the qualifiers. Part of this process involves
|
89
|
+
# querying the Watir driver library to determine what qualifiers
|
90
|
+
# it handles natively. Consider the following:
|
91
|
+
#
|
92
|
+
# select_list :accounts, id: 'accounts', selected: 'Select Option'
|
93
|
+
#
|
94
|
+
# Given that, this method will return with the following:
|
95
|
+
#
|
96
|
+
# locator_args: {:id=>"accounts"}
|
97
|
+
# qualifier_args: {:selected=>"Select Option"}
|
98
|
+
#
|
99
|
+
# Consider this:
|
100
|
+
#
|
101
|
+
# p :login_form, id: 'open', index: 0, visible: true
|
102
|
+
#
|
103
|
+
# Given that, this method will return with the following:
|
104
|
+
#
|
105
|
+
# locator_args: {:id=>"open", :index=>0, :visible=>true}
|
106
|
+
# qualifier_args: {}
|
107
|
+
#
|
108
|
+
# Notice that the `visible` qualifier is part of the locator arguments
|
109
|
+
# as opposed to being a qualifier argument, like `selected` was in the
|
110
|
+
# previous example. This is because Watir 6.x handles the `visible`
|
111
|
+
# qualifier natively. "Handling natively" means that when a qualifier
|
112
|
+
# is part of the locator, Watir knows how to intrpret the qualifier
|
113
|
+
# as a condition on the element, not as a way to locate the element.
|
114
|
+
def gather_aspects(identifier, element, locator_args, qualifier_args)
|
115
|
+
identifier.each_with_index do |hashes, index|
|
116
|
+
next if hashes.nil? || hashes.is_a?(Proc)
|
117
|
+
|
118
|
+
hashes.each do |k, v|
|
119
|
+
methods = Watir.element_class_for(element).instance_methods
|
120
|
+
if methods.include?(:"#{k}?") && !NATIVE_QUALIFIERS.include?(k)
|
121
|
+
qualifier_args[k] = identifier[index][k]
|
122
|
+
else
|
123
|
+
locator_args[k] = v
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
[locator_args, qualifier_args]
|
128
|
+
end
|
48
129
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
130
|
+
# Defines an accessor method for an element that allows the "friendly
|
131
|
+
# name" (identifier) of the element to be proxied to a Watir element
|
132
|
+
# object that corresponds to the element type. When this identifier
|
133
|
+
# is referenced, it generates an accessor method for that element
|
134
|
+
# in the browser. Consider this element definition defined on a class
|
135
|
+
# with an instance of `page`:
|
136
|
+
#
|
137
|
+
# text_field :username, id: 'username'
|
138
|
+
#
|
139
|
+
# This allows:
|
140
|
+
#
|
141
|
+
# page.username.set 'tester'
|
142
|
+
#
|
143
|
+
# So any element identifier can be called as if it were a method on
|
144
|
+
# the interface (class) on which it is defined. Because the method
|
145
|
+
# is proxied to Watir, you can use the full Watir API by calling
|
146
|
+
# methods (like `set`, `click`, etc) on the element identifier.
|
147
|
+
#
|
148
|
+
# It is also possible to have an element definition like this:
|
149
|
+
#
|
150
|
+
# text_field :password
|
151
|
+
#
|
152
|
+
# This would allow access like this:
|
153
|
+
#
|
154
|
+
# page.username(id: 'username').set 'tester'
|
155
|
+
#
|
156
|
+
# This approach would lead to the *values variable having an array
|
157
|
+
# like this: [{:id => 'username'}].
|
158
|
+
#
|
159
|
+
# A third approach would be to utilize one element definition within
|
160
|
+
# the context of another. Consider the following element definitions:
|
161
|
+
#
|
162
|
+
# article :practice, id: 'practice'
|
163
|
+
#
|
164
|
+
# a :page_link do |text|
|
165
|
+
# practice.a(text: text)
|
166
|
+
# end
|
167
|
+
#
|
168
|
+
# This would allow access like this:
|
169
|
+
#
|
170
|
+
# page.page_link('Drag and Drop').click
|
171
|
+
#
|
172
|
+
# This approach would lead to the *values variable having an array
|
173
|
+
# like this: ["Drag and Drop"].
|
174
|
+
def define_element_accessor(identifier, *signature, element, &block)
|
175
|
+
locators, qualifiers = accessor_aspects(element, signature)
|
176
|
+
define_method(identifier.to_s) do |*values|
|
177
|
+
if block_given?
|
178
|
+
instance_exec(*values, &block)
|
179
|
+
else
|
180
|
+
locators = values[0] if locators.empty?
|
181
|
+
access_element(element, locators, qualifiers)
|
182
|
+
end
|
183
|
+
end
|
53
184
|
end
|
54
185
|
end
|
55
186
|
end
|
data/lib/testable/errors.rb
CHANGED
@@ -4,8 +4,12 @@ module Testable
|
|
4
4
|
NoUrlMatchForDefinition = Class.new(StandardError)
|
5
5
|
NoUrlMatchPossible = Class.new(StandardError)
|
6
6
|
NoTitleForDefinition = Class.new(StandardError)
|
7
|
-
NoLocatorForDefinition = Class.new(StandardError)
|
8
7
|
PageNotValidatedError = Class.new(StandardError)
|
9
|
-
|
8
|
+
|
9
|
+
class PageURLFromFactoryNotVerified < StandardError
|
10
|
+
def message
|
11
|
+
"The page URL was not verified during a factory setup."
|
12
|
+
end
|
13
|
+
end
|
10
14
|
end
|
11
15
|
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
class Object
|
2
|
+
# This method is necessary to dynamically chain method calls. The reason
|
3
|
+
# this is necessary the data setter initially has no idea of the actual
|
4
|
+
# object it's going to be dealing with, particularly because part of its
|
5
|
+
# job is to find that object and map a data string to it. Not only this,
|
6
|
+
# but that element will have been called on a specific instance of a
|
7
|
+
# interface class. With the example provide in the comments below for the
|
8
|
+
# `using` method, the following would be the case:
|
9
|
+
#
|
10
|
+
# method_chain: warp_factor.set
|
11
|
+
# o (object): <WarpTravel:0x007f8b23224218>
|
12
|
+
# m (method): warp_factor
|
13
|
+
# data: 1
|
14
|
+
#
|
15
|
+
# Thus what you end up with is:
|
16
|
+
#
|
17
|
+
# <WarpTravel:0x007f8b23224218>.warp_factor.set 1
|
18
|
+
def chain(method_chain, data = nil)
|
19
|
+
return self if method_chain.empty?
|
20
|
+
|
21
|
+
method_chain.split('.').inject(self) do |o, m|
|
22
|
+
if data.nil?
|
23
|
+
o.send(m.intern)
|
24
|
+
else
|
25
|
+
o.send(m.intern, data)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module Testable
|
32
|
+
module DataSetter
|
33
|
+
# The `using` method tells Testable to match up whatever data is passed
|
34
|
+
# in via the action with element definitions. If those elements are found,
|
35
|
+
# they will be populated with the specified data. Consider the following:
|
36
|
+
#
|
37
|
+
# class WarpTravel
|
38
|
+
# include Testable
|
39
|
+
#
|
40
|
+
# text_field :warp_factor, id: 'warpInput'
|
41
|
+
# text_field :velocity, id: 'velocityInput'
|
42
|
+
# text_field :distance, id: 'distInput'
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# Assuming an instance of this class called `page`, you could do the
|
46
|
+
# following:
|
47
|
+
#
|
48
|
+
# page.using_data(warp_factor: 1, velocity: 1, distance: 4.3)
|
49
|
+
#
|
50
|
+
# This is based on conventions. The idea is that element definitions are
|
51
|
+
# written in the form of "snake case" -- meaning, underscores between
|
52
|
+
# each separate word. In the above example, "warp_factor: 1" would be
|
53
|
+
# matched to the `warp_factor` element and the value used for that
|
54
|
+
# element would be "1". The default operation for a text field is to
|
55
|
+
# enter the value in. It is also possible to use strings:
|
56
|
+
#
|
57
|
+
# page.using_data("warp factor": 1, velocity: 1, distance: 4.3)
|
58
|
+
#
|
59
|
+
# Here "warp factor" would be converted to "warp_factor".
|
60
|
+
def using(data)
|
61
|
+
data.each do |key, value|
|
62
|
+
use_data_with(key, value) if object_enabled_for(key)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
alias using_data using
|
67
|
+
alias use_data using
|
68
|
+
alias using_values using
|
69
|
+
alias use_values using
|
70
|
+
alias use using
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
# This is the method that is delegated to in order to make sure that
|
75
|
+
# elements are interacted with appropriately. This will in turn delegate
|
76
|
+
# to `set_and_select` and `check_and_uncheck`, which determines what
|
77
|
+
# actions are viable based on the type of element that is being dealt
|
78
|
+
# with. These aspects are what tie this particular implementation to
|
79
|
+
# Watir.
|
80
|
+
def use_data_with(key, value)
|
81
|
+
element = send(key.to_s.tr(' ', '_'))
|
82
|
+
set_and_select(key, element, value)
|
83
|
+
check_and_uncheck(key, element, value)
|
84
|
+
end
|
85
|
+
|
86
|
+
def set_and_select(key, element, value)
|
87
|
+
key = key.to_s.tr(' ', '_')
|
88
|
+
chain("#{key}.set", value) if element.class == Watir::TextField
|
89
|
+
chain("#{key}.set") if element.class == Watir::Radio
|
90
|
+
chain("#{key}.select", value) if element.class == Watir::Select
|
91
|
+
end
|
92
|
+
|
93
|
+
def check_and_uncheck(key, element, value)
|
94
|
+
key = key.to_s.tr(' ', '_')
|
95
|
+
return chain("#{key}.check") if element.class == Watir::CheckBox && value
|
96
|
+
|
97
|
+
chain("#{key}.uncheck") if element.class == Watir::CheckBox
|
98
|
+
end
|
99
|
+
|
100
|
+
# This is a sanity check method to make sure that whatever element is
|
101
|
+
# being used as part of the data setting, it exists in the DOM, is
|
102
|
+
# visible (meaning, display is not 'none'), and is capable of accepting
|
103
|
+
# input, thus being enabled.
|
104
|
+
def object_enabled_for(key)
|
105
|
+
web_element = send(key.to_s.tr(' ', '_'))
|
106
|
+
web_element.enabled? && web_element.present?
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -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);
|