testable 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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);
|