testable 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
- def elements
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
- module Element
22
- Testable.elements.each do |element|
23
- # This is what allows Watir-based elements to be defined
24
- # on an element definition.
25
- define_method(element) do |*signature|
26
- identifier, locator = parse_signature(signature)
27
- define_element_accessor(identifier, locator, element)
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
- private
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
- def define_element_accessor(identifier, *locator, element)
34
- # This is what allows each identifier to be a method.
35
- define_method(identifier.to_s.to_sym) do |*values|
36
- no_locator(self.class, identifier) if empty_locator(locator, values)
37
- locator = values if locator[0].nil?
38
- reference_element(element, locator)
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
- def parse_signature(signature)
43
- [signature.shift, signature.shift]
44
- end
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
- module Locator
47
- private
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
- def reference_element(element, locator)
50
- @browser.__send__(element, *locator).to_subtype
51
- rescue NoMethodError
52
- @browser.__send__(element, *locator)
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
@@ -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
- NoBlockForWhenReady = Class.new(StandardError)
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
- // WebDriver arguments
2
- var element = arguments[0];
3
- var delay = arguments[1] * 1000;
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
- // Observer
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);