testable 0.6.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.hound.yml +25 -4
- data/Gemfile +0 -2
- data/README.md +25 -2
- data/lib/testable.rb +67 -6
- data/lib/testable/attribute.rb +22 -0
- data/lib/testable/deprecator.rb +6 -0
- data/lib/testable/element.rb +60 -7
- data/lib/testable/errors.rb +14 -0
- data/lib/testable/extensions/core_ruby.rb +4 -0
- data/lib/testable/extensions/data_setter.rb +6 -4
- data/lib/testable/extensions/dom_observer.rb +2 -0
- data/lib/testable/locator.rb +46 -4
- data/lib/testable/logger.rb +7 -3
- data/lib/testable/region.rb +173 -0
- data/lib/testable/version.rb +6 -2
- data/testable.gemspec +2 -5
- metadata +10 -58
- data/examples/testable-capybara-context.rb +0 -64
- data/examples/testable-capybara-rspec.rb +0 -70
- data/examples/testable-capybara.rb +0 -46
- data/lib/testable/capybara/dsl.rb +0 -82
- data/lib/testable/capybara/node.rb +0 -30
- data/lib/testable/capybara/page.rb +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7eb4a3420ccd34f95f57425fdd3575900c1c47d46235cba1d0b8554779a0a62a
|
4
|
+
data.tar.gz: 8282d00a7c70a5ccc49a68f1e4eea23c6c0ff94955f6aad9ec4654931e9cd762
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b4f74bec001c20d76044ea642f8686b49ef4171ea6e71548516627adcdbc6a52f3fb17ed2c2ec43b78a4d1d85fd3022a3cf10e17a2fcd841f89a34a174fe66f1
|
7
|
+
data.tar.gz: a10187fd520ee7c7cc22b8c4cc6b4d6f622410f305d6ff1a1587ba0f4ae4047e4d409fc5e17e45f909828355337be312fcdbcb1465357a5340eb8e9e8d99a00f
|
data/.hound.yml
CHANGED
@@ -1,9 +1,22 @@
|
|
1
1
|
AllCops:
|
2
|
+
EnabledByDefault: true
|
2
3
|
Exclude:
|
3
4
|
- testable.gemspec
|
4
5
|
- spec/**/*
|
5
6
|
- examples/**/*
|
6
7
|
|
8
|
+
# For now, it is permissible to disable and enable Rubocop checks.
|
9
|
+
Style/DisableCopsWithinSourceCodeDirective:
|
10
|
+
Enabled: false
|
11
|
+
|
12
|
+
# Do not require parentheses with methods that take arguments.
|
13
|
+
Style/MethodCallWithArgsParentheses:
|
14
|
+
Enabled: false
|
15
|
+
|
16
|
+
# Do not enforce copyright messaging.
|
17
|
+
Style/Copyright:
|
18
|
+
Enabled: false
|
19
|
+
|
7
20
|
# Removing need for frozen string literal comment.
|
8
21
|
Style/FrozenStringLiteralComment:
|
9
22
|
Enabled: false
|
@@ -30,11 +43,11 @@ Style/SignalException:
|
|
30
43
|
Enabled: false
|
31
44
|
|
32
45
|
# This never works for validations.
|
33
|
-
Layout/
|
46
|
+
Layout/HashAlignment:
|
34
47
|
EnforcedLastArgumentHashStyle: ignore_implicit
|
35
48
|
|
36
49
|
# Align multi-line params with previous line.
|
37
|
-
Layout/
|
50
|
+
Layout/ParameterAlignment:
|
38
51
|
EnforcedStyle: with_fixed_indentation
|
39
52
|
|
40
53
|
# Indent `when` clause one step from `case`.
|
@@ -59,7 +72,7 @@ Metrics/MethodLength:
|
|
59
72
|
|
60
73
|
# Encourage short (as possible) modules.
|
61
74
|
Metrics/ModuleLength:
|
62
|
-
Max:
|
75
|
+
Max: 105
|
63
76
|
|
64
77
|
# Encourage fewer parameters.
|
65
78
|
Metrics/ParameterLists:
|
@@ -69,11 +82,19 @@ Metrics/ParameterLists:
|
|
69
82
|
Lint/ScriptPermission:
|
70
83
|
Enabled: false
|
71
84
|
|
85
|
+
# Not requiring fully qualified constants.
|
86
|
+
Lint/ConstantResolution:
|
87
|
+
Enabled: false
|
88
|
+
|
89
|
+
# Do not require gem descriptions
|
90
|
+
Bundler/GemComment:
|
91
|
+
Enabled: false
|
92
|
+
|
72
93
|
# Testable Exceptions
|
73
94
|
|
74
95
|
# Allow methods with has_ for predicates.
|
75
96
|
Naming/PredicateName:
|
76
|
-
|
97
|
+
AllowedMethods:
|
77
98
|
- has_correct_title?
|
78
99
|
- has_correct_url?
|
79
100
|
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -14,7 +14,7 @@
|
|
14
14
|
[![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://rubydoc.info/github/jeffnyman/testable/master/frames)
|
15
15
|
[![Inline docs](http://inch-ci.org/github/jeffnyman/testable.png)](http://inch-ci.org/github/jeffnyman/github)
|
16
16
|
|
17
|
-
Testable is an automated test micro-framework that provides a thin wrapper around [Watir](http://watir.com/)
|
17
|
+
Testable is an automated test micro-framework that provides a thin wrapper around [Watir](http://watir.com/). Testable is based on many ideas from tools like [SitePrism](https://github.com/natritmeyer/site_prism) and [Watirsome](https://github.com/p0deje/watirsome), while also being a logical evolution of my own tool, [Tapestry](https://github.com/jeffnyman/tapestry).
|
18
18
|
|
19
19
|
One of the core goals of Testable is to be a mediating influence between higher-level tests (acceptance criteria) and lower-level implementations of those tests. You can see some of the design principles for more details on what guided construction.
|
20
20
|
|
@@ -60,7 +60,15 @@ An automated test framework provides a machine-executable abstraction around tes
|
|
60
60
|
|
61
61
|
One of the obstacles to covering the gap between principles of testing and the practice of testing is the mechanics of writing tests. These mechanics are focused on abstractions. A lot of the practice of testing comes down to that: finding the right abstractions.
|
62
62
|
|
63
|
-
An automated test framework should be capable of consuming your preferred abstractions because ultimately the automation is simply a tool that supports testing, which means how the framework encourages tests to be expressed should have high fidelity with how human tests would be expressed.
|
63
|
+
An automated test framework should be capable of consuming your preferred abstractions because ultimately the automation is simply a tool that supports testing, which means how the framework encourages tests to be expressed should have high fidelity with how human tests would be expressed. The execution of tests, whether automated or not, is often secondary to the design of those tests in the first place. Testable was designed around a couple of truisms:
|
64
|
+
|
65
|
+
* When tests become automation:
|
66
|
+
* We risk turning testing into a programming problem.
|
67
|
+
|
68
|
+
* When tests become automation:
|
69
|
+
* We risk the mechanisms overwhelming the meaning.
|
70
|
+
|
71
|
+
This is why Testable is designed as a _micro_-framework rather than a framework.
|
64
72
|
|
65
73
|
Testable is built around the the idea that automation should largely be small-footprint, low-fiction, and high-yield.
|
66
74
|
|
@@ -68,6 +76,21 @@ The code that a test-supporting micro-framework allows should be modular, promot
|
|
68
76
|
|
69
77
|
That makes the automation code less expensive to maintain and easier to change. That, ultimately, has a positive impact on the cost of change but, more importantly, allows Testable to be fit within a cost of mistake model, where the goal is to get feedback as quickly as possible regarding when mistakes are made.
|
70
78
|
|
79
|
+
Some of the core principles of Testable's design are the following:
|
80
|
+
|
81
|
+
* Embrace small code.
|
82
|
+
* Abstraction encourages clarity.
|
83
|
+
* No computation is too small to be put into a helper function.
|
84
|
+
* No expression is too simple to be given a name.
|
85
|
+
* Small code is more easily seen to be obviously correct.
|
86
|
+
* Code that's more obviously correct can be more easily composed.
|
87
|
+
|
88
|
+
The above also led to some secondary, but no less important, principles of design:
|
89
|
+
|
90
|
+
* Be willing to trade elegance of design for practicality of implementation.
|
91
|
+
* Embrace brevity, but do not sacrifice readability. Concise, not terse.
|
92
|
+
* Prefer elegance over efficiency where efficiency is less than critical.
|
93
|
+
|
71
94
|
## Development
|
72
95
|
|
73
96
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake spec:all` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/lib/testable.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require "testable/version"
|
2
2
|
require "testable/page"
|
3
3
|
require "testable/ready"
|
4
|
+
require "testable/region"
|
4
5
|
require "testable/logger"
|
5
6
|
require "testable/context"
|
6
7
|
require "testable/element"
|
@@ -8,33 +9,71 @@ require "testable/locator"
|
|
8
9
|
require "testable/attribute"
|
9
10
|
require "testable/deprecator"
|
10
11
|
|
11
|
-
require "testable/capybara/page"
|
12
|
-
|
13
12
|
require "testable/extensions/core_ruby"
|
14
13
|
require "testable/extensions/data_setter"
|
15
14
|
require "testable/extensions/dom_observer"
|
16
15
|
|
17
16
|
require "watir"
|
18
|
-
require "capybara"
|
19
|
-
require "webdrivers"
|
20
17
|
|
21
18
|
module Testable
|
19
|
+
# This is the core method of Testable which makes the various components
|
20
|
+
# of the framework available to tests.
|
22
21
|
def self.included(caller)
|
23
22
|
caller.extend Testable::Pages::Attribute
|
24
23
|
caller.extend Testable::Pages::Element
|
24
|
+
caller.extend Testable::Pages::Region
|
25
25
|
caller.__send__ :include, Testable::Ready
|
26
26
|
caller.__send__ :include, Testable::Pages
|
27
27
|
caller.__send__ :include, Testable::Element::Locator
|
28
28
|
caller.__send__ :include, Testable::DataSetter
|
29
29
|
end
|
30
30
|
|
31
|
-
|
31
|
+
# Initialization is used to establish a browser instance within the
|
32
|
+
# context of the framework. The logic here requires determining under
|
33
|
+
# what condition a browser instance may have been set.
|
34
|
+
def initialize(browser = nil, region_element = nil, parent = nil, &block)
|
32
35
|
@browser = Testable.browser unless Testable.browser.nil?
|
33
36
|
@browser = browser if Testable.browser.nil?
|
34
|
-
|
37
|
+
@region_element = region_element || Testable.browser
|
38
|
+
@parent = parent
|
39
|
+
definition_setup
|
35
40
|
instance_eval(&block) if block
|
36
41
|
end
|
37
42
|
|
43
|
+
# This method is used to setup any definitions provided as part of the
|
44
|
+
# executable of Testable. This generally means any page definitions that
|
45
|
+
# include Testable.
|
46
|
+
def definition_setup
|
47
|
+
begin_with if respond_to?(:begin_with)
|
48
|
+
initialize_page if respond_to?(:initialize_page)
|
49
|
+
initialize_region if respond_to?(:initialize_region)
|
50
|
+
initialize_regions
|
51
|
+
end
|
52
|
+
|
53
|
+
# This method is used to interate through any definitions that provide
|
54
|
+
# an "initialize_region" method. What happens here is essentially that
|
55
|
+
# new polymorphic modules are created based on classes. This is what
|
56
|
+
# allows a region to be part of a page definition, the latter of which
|
57
|
+
# is nothing more than a standard Ruby class.
|
58
|
+
def initialize_regions
|
59
|
+
@initialized_regions ||= []
|
60
|
+
|
61
|
+
# The goal here is get all included and extended modules.
|
62
|
+
modules = self.class.included_modules + (class << self; self end).included_modules
|
63
|
+
modules.uniq!
|
64
|
+
|
65
|
+
# Then each of those modules can be initialized.
|
66
|
+
modules.each do |m|
|
67
|
+
# First a check is made the that constructor is defined and that it has
|
68
|
+
# not been called before.
|
69
|
+
next if @initialized_regions.include?(m) || !m.instance_methods.include?(:initialize_region)
|
70
|
+
|
71
|
+
m.instance_method(:initialize_region).bind(self).call
|
72
|
+
|
73
|
+
@initialized_regions << m
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
38
77
|
# This accessor is needed so that internal API calls, like `markup` or
|
39
78
|
# `text`, have access to the browser instance. This is also necessary
|
40
79
|
# in order for element handling to be called appropriately on the a
|
@@ -42,6 +81,14 @@ module Testable
|
|
42
81
|
# browser Testable is using.
|
43
82
|
attr_accessor :browser
|
44
83
|
|
84
|
+
# This accessor is needed so that the region_element is recognized as
|
85
|
+
# part of the overall execution context of Testable.
|
86
|
+
attr_reader :region_element
|
87
|
+
|
88
|
+
# This accessor is needed so that the parent of a region is recognized as
|
89
|
+
# part of the overall execution context of Testable.
|
90
|
+
attr_reader :parent
|
91
|
+
|
45
92
|
class << self
|
46
93
|
# Provides a means to allow a configure block on Testable. This allows you
|
47
94
|
# to setup Testable, as such:
|
@@ -131,23 +178,29 @@ module Testable
|
|
131
178
|
#
|
132
179
|
# Watir.logger.output = "wire.log"
|
133
180
|
|
181
|
+
# Path for the logger to use.
|
134
182
|
def wire_path=(logdev)
|
135
183
|
Watir.logger.reopen(logdev)
|
136
184
|
end
|
137
185
|
|
186
|
+
# Level of logging.
|
138
187
|
def wire_level_logging=(value)
|
139
188
|
Watir.logger.level = value
|
140
189
|
end
|
141
190
|
|
191
|
+
# Report the current logging level.
|
142
192
|
def wire_level_logging
|
143
193
|
%i[DEBUG INFO WARN ERROR FATAL UNKNOWN][Watir.logger.level]
|
144
194
|
end
|
145
195
|
|
196
|
+
# Returns information about the API that Watir exposes. This does not
|
197
|
+
# include Selenium elements, which Watir is wrapping.
|
146
198
|
def watir_api
|
147
199
|
browser.methods - Object.public_methods -
|
148
200
|
Watir::Container.instance_methods
|
149
201
|
end
|
150
202
|
|
203
|
+
# Returns information about the API that is specific to Selenium.
|
151
204
|
def selenium_api
|
152
205
|
browser.driver.methods - Object.public_methods
|
153
206
|
end
|
@@ -157,6 +210,9 @@ module Testable
|
|
157
210
|
# access to the browser.
|
158
211
|
attr_accessor :browser
|
159
212
|
|
213
|
+
# This sets the browser instance so that Testable is aware of what that
|
214
|
+
# instance is. This is how a test, using the Testable framework, will
|
215
|
+
# be able to know which browser to communicate with.
|
160
216
|
def set_browser(app = :chrome, *args)
|
161
217
|
@browser = Watir::Browser.new(app, *args)
|
162
218
|
Testable.browser = @browser
|
@@ -164,10 +220,15 @@ module Testable
|
|
164
220
|
|
165
221
|
alias start_browser set_browser
|
166
222
|
|
223
|
+
# Generates a quit event on the browser instance. This is passed through
|
224
|
+
# to Watir which will ultimately delegate browser handling to Selenium
|
225
|
+
# and thus WebDriver.
|
167
226
|
def quit_browser
|
168
227
|
@browser.quit
|
169
228
|
end
|
170
229
|
|
230
|
+
# This provides the API that is defined by Testable when you have a
|
231
|
+
# working model, such as a page object.
|
171
232
|
def api
|
172
233
|
methods - Object.public_methods
|
173
234
|
end
|
data/lib/testable/attribute.rb
CHANGED
@@ -5,31 +5,53 @@ module Testable
|
|
5
5
|
module Attribute
|
6
6
|
include Situation
|
7
7
|
|
8
|
+
# This is an attribute that can be specified on a model, such as a page
|
9
|
+
# object. When this attribute is provided, the model will have a URL
|
10
|
+
# that can be navigated to automatically by Testable.
|
8
11
|
def url_is(url = nil)
|
9
12
|
url_is_empty if url.nil? && url_attribute.nil?
|
10
13
|
url_is_empty if url.nil? || url.empty?
|
11
14
|
@url = url
|
12
15
|
end
|
13
16
|
|
17
|
+
# This allows a test to query for the url attribute that was set on the
|
18
|
+
# model. It's important to note that this is not the actual URL in the
|
19
|
+
# browser, but rather whatever was created on the model.
|
14
20
|
def url_attribute
|
15
21
|
@url
|
16
22
|
end
|
17
23
|
|
24
|
+
# This is an attribute that can be specified on a model, such as a page
|
25
|
+
# object. When this attribute is provided, the model will have a way to
|
26
|
+
# match the URL in the browser with the matcher provided via this
|
27
|
+
# attribute.
|
18
28
|
def url_matches(pattern = nil)
|
19
29
|
url_match_is_empty if pattern.nil?
|
20
30
|
url_match_is_empty if pattern.is_a?(String) && pattern.empty?
|
21
31
|
@url_match = pattern
|
22
32
|
end
|
23
33
|
|
34
|
+
# This allows a test to query for the url match attribute that was set
|
35
|
+
# on the model. This does not perform a match or do anything to check
|
36
|
+
# if a URL is matching. This simply returns the attribute that was
|
37
|
+
# provided for the URL matching.
|
24
38
|
def url_match_attribute
|
25
39
|
@url_match
|
26
40
|
end
|
27
41
|
|
42
|
+
# This is an attribute that can be specified on a model, such as a page
|
43
|
+
# object. When this attribute is provided, the model will have a bit of
|
44
|
+
# text that provides information about what the title of a browser page
|
45
|
+
# should be. This is not checking the title; it's simply providing the
|
46
|
+
# text that will be checked against the actual title.
|
28
47
|
def title_is(title = nil)
|
29
48
|
title_is_empty if title.nil? || title.empty?
|
30
49
|
@title = title
|
31
50
|
end
|
32
51
|
|
52
|
+
# This allows a test to query for the title attribute that was set on
|
53
|
+
# the model. It's important to note that this is not the actual title
|
54
|
+
# in the browser, but rather whatever was created on the model.
|
33
55
|
def title_attribute
|
34
56
|
@title
|
35
57
|
end
|
data/lib/testable/deprecator.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
module Testable
|
2
2
|
class Deprecator
|
3
3
|
class << self
|
4
|
+
# This is used to indicate that certain functionality within Testable
|
5
|
+
# has been deprecated and the previous functionality will disappear.
|
4
6
|
def deprecate(current, upcoming = nil, known_version = nil)
|
5
7
|
if upcoming
|
6
8
|
warn(
|
@@ -16,6 +18,10 @@ module Testable
|
|
16
18
|
) if known_version
|
17
19
|
end
|
18
20
|
|
21
|
+
# This is used to indicate that certain functionality within Testable
|
22
|
+
# has been soft deprecated, meaning that some aspect of how the
|
23
|
+
# framework is configured has changed and that change will become
|
24
|
+
# the new default behavior in a given version.
|
19
25
|
def soft_deprecate(current, reason, known_version, upcoming = nil)
|
20
26
|
debug("The #{current} method is changing and is now configurable.")
|
21
27
|
debug("REASON: #{reason}.")
|
data/lib/testable/element.rb
CHANGED
@@ -4,19 +4,69 @@ module Testable
|
|
4
4
|
module_function
|
5
5
|
|
6
6
|
NATIVE_QUALIFIERS = %i[visible].freeze
|
7
|
+
private_constant :NATIVE_QUALIFIERS
|
7
8
|
|
9
|
+
# This predicate method returns all of the elements that can be recognized
|
10
|
+
# by Watir.
|
8
11
|
def elements?
|
9
12
|
@elements
|
10
13
|
end
|
11
14
|
|
15
|
+
# This predicate method checks if a given element is in the list of known
|
16
|
+
# elements that Watir knows how to interact with.
|
12
17
|
def recognizes?(method)
|
13
18
|
@elements.include? method.to_sym
|
14
19
|
end
|
15
20
|
|
21
|
+
# Provides a list of all elements that Watir knows how to work with. This
|
22
|
+
# is generated from the Watir container itself so the list should always
|
23
|
+
# be up to date.
|
16
24
|
def elements
|
17
25
|
@elements ||= Watir::Container.instance_methods unless @elements
|
18
26
|
end
|
19
27
|
|
28
|
+
# This predicate method checks if a given element is a valid element that
|
29
|
+
# Watir knows how to interact with, but in plural form.
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# Testable.recognizes?(:spans) # => true
|
33
|
+
# Testable.recognizes?(:divs) # => true
|
34
|
+
# Testable.recognizes?(:class) # => false
|
35
|
+
# Testable.recognizes?(:classes) # => false
|
36
|
+
def plural?(method)
|
37
|
+
value = method.to_s
|
38
|
+
plural = value.to_sym
|
39
|
+
|
40
|
+
# Remove the trailing "s" or "es" to de-pluralize.
|
41
|
+
single = value.sub(/e?s$/, '').to_sym
|
42
|
+
|
43
|
+
!value.match(/s$/).nil? &&
|
44
|
+
@elements.include?(plural) &&
|
45
|
+
@elements.include?(single)
|
46
|
+
end
|
47
|
+
|
48
|
+
# This method will allow a particular element name to be pluralized.
|
49
|
+
#
|
50
|
+
# @example
|
51
|
+
# Empirical.pluralize(:div) => divs
|
52
|
+
# Empirical.pluralize(:checkbox) => checkboxes
|
53
|
+
def self.pluralize(method)
|
54
|
+
value = method.to_s
|
55
|
+
|
56
|
+
# This bit is not what you would call optimized. Essentially, it just
|
57
|
+
# tries to ppluralize with "s" and see if that method is included in the
|
58
|
+
# Watir methods. If that fails, then an attempt to pluralize with "es" is
|
59
|
+
# tried. If that fails, the assumption is made that a pluralized form of
|
60
|
+
# the Watir method does not exist.
|
61
|
+
if @elements.include?(:"#{value}s")
|
62
|
+
:"#{value}s"
|
63
|
+
elsif @elements.include?(:"#{value}es")
|
64
|
+
:"#{value}es"
|
65
|
+
else
|
66
|
+
raise Testable::Errors::PluralizedElementError, "Unable to find plural form for #{value}!"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
20
70
|
module Pages
|
21
71
|
module Element
|
22
72
|
# This iterator goes through the Watir container methods and
|
@@ -173,14 +223,17 @@ module Testable
|
|
173
223
|
# like this: ["Drag and Drop"].
|
174
224
|
def define_element_accessor(identifier, *signature, element, &block)
|
175
225
|
locators, qualifiers = accessor_aspects(element, signature)
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
226
|
+
|
227
|
+
include(Module.new do
|
228
|
+
define_method(identifier.to_s) do |*values|
|
229
|
+
if block_given?
|
230
|
+
instance_exec(*values, &block)
|
231
|
+
else
|
232
|
+
locators = values[0] if locators.empty?
|
233
|
+
access_element(element, locators, qualifiers)
|
234
|
+
end
|
182
235
|
end
|
183
|
-
end
|
236
|
+
end)
|
184
237
|
end
|
185
238
|
end
|
186
239
|
end
|