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.
@@ -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);