testable 0.3.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.gitignore +37 -25
- data/.hound.yml +31 -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 +36 -17
- data/Rakefile +52 -11
- data/bin/console +2 -2
- data/bin/setup +0 -0
- data/examples/testable-capybara-context.rb +64 -0
- data/examples/testable-capybara-rspec.rb +70 -0
- data/examples/testable-capybara.rb +46 -0
- 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 +80 -0
- data/examples/testable-watir.rb +118 -0
- data/lib/testable.rb +142 -10
- data/lib/testable/attribute.rb +38 -0
- data/lib/testable/capybara/dsl.rb +82 -0
- data/lib/testable/capybara/node.rb +30 -0
- data/lib/testable/capybara/page.rb +29 -0
- data/lib/testable/context.rb +73 -0
- data/lib/testable/deprecator.rb +40 -0
- data/lib/testable/element.rb +162 -31
- data/lib/testable/errors.rb +6 -2
- data/lib/testable/extensions/core_ruby.rb +13 -0
- data/lib/testable/extensions/data_setter.rb +144 -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/logger.rb +16 -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/testable.gemspec +19 -9
- metadata +90 -23
- 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
|
data/lib/testable.rb
CHANGED
@@ -1,34 +1,160 @@
|
|
1
1
|
require "testable/version"
|
2
|
-
|
2
|
+
require "testable/page"
|
3
3
|
require "testable/ready"
|
4
|
-
require "testable/
|
4
|
+
require "testable/logger"
|
5
|
+
require "testable/context"
|
5
6
|
require "testable/element"
|
6
|
-
require "testable/
|
7
|
-
require "testable/
|
8
|
-
require "testable/
|
7
|
+
require "testable/locator"
|
8
|
+
require "testable/attribute"
|
9
|
+
require "testable/deprecator"
|
10
|
+
|
11
|
+
require "testable/capybara/page"
|
12
|
+
|
13
|
+
require "testable/extensions/core_ruby"
|
14
|
+
require "testable/extensions/data_setter"
|
15
|
+
require "testable/extensions/dom_observer"
|
9
16
|
|
10
17
|
require "watir"
|
11
|
-
require "
|
18
|
+
require "capybara"
|
19
|
+
require "webdrivers"
|
12
20
|
|
13
21
|
module Testable
|
14
22
|
def self.included(caller)
|
15
|
-
caller.extend Testable::
|
16
|
-
caller.extend Testable::
|
23
|
+
caller.extend Testable::Pages::Attribute
|
24
|
+
caller.extend Testable::Pages::Element
|
17
25
|
caller.__send__ :include, Testable::Ready
|
18
|
-
caller.__send__ :include, Testable::
|
19
|
-
caller.__send__ :include, Testable::Interface::Page
|
26
|
+
caller.__send__ :include, Testable::Pages
|
20
27
|
caller.__send__ :include, Testable::Element::Locator
|
28
|
+
caller.__send__ :include, Testable::DataSetter
|
21
29
|
end
|
22
30
|
|
23
31
|
def initialize(browser = nil, &block)
|
24
32
|
@browser = Testable.browser unless Testable.browser.nil?
|
25
33
|
@browser = browser if Testable.browser.nil?
|
34
|
+
begin_with if respond_to?(:begin_with)
|
26
35
|
instance_eval(&block) if block
|
27
36
|
end
|
28
37
|
|
38
|
+
# This accessor is needed so that internal API calls, like `markup` or
|
39
|
+
# `text`, have access to the browser instance. This is also necessary
|
40
|
+
# in order for element handling to be called appropriately on the a
|
41
|
+
# valid browser instance. This is an instance-level access to whatever
|
42
|
+
# browser Testable is using.
|
29
43
|
attr_accessor :browser
|
30
44
|
|
31
45
|
class << self
|
46
|
+
# Provides a means to allow a configure block on Testable. This allows you
|
47
|
+
# to setup Testable, as such:
|
48
|
+
#
|
49
|
+
# Testable.configure do |config|
|
50
|
+
# config.driver_timeout = 5
|
51
|
+
# config.wire_level_logging = :info
|
52
|
+
# config.log_level = :debug
|
53
|
+
# end
|
54
|
+
def configure
|
55
|
+
yield self
|
56
|
+
end
|
57
|
+
|
58
|
+
# Watir provides a default timeout of 30 seconds. This allows you to change
|
59
|
+
# that in the Testable context. For example:
|
60
|
+
#
|
61
|
+
# Testable.driver_timeout = 5
|
62
|
+
#
|
63
|
+
# This would equivalent to doing this:
|
64
|
+
#
|
65
|
+
# Watir.default_timeout = 5
|
66
|
+
def driver_timeout=(value)
|
67
|
+
Watir.default_timeout = value
|
68
|
+
end
|
69
|
+
|
70
|
+
# The Testable logger object. To log messages:
|
71
|
+
#
|
72
|
+
# Testable.logger.info('Some information.')
|
73
|
+
# Testable.logger.debug('Some diagnostics')
|
74
|
+
#
|
75
|
+
# To alter or check the current logging level, you can call `.log_level=`
|
76
|
+
# or `.log_level`. By default the logger will output all messages to the
|
77
|
+
# standard output ($stdout) but it can be altered to log to a file or to
|
78
|
+
# another IO location by calling `.log_path=`.
|
79
|
+
def logger
|
80
|
+
@logger ||= Testable::Logger.new.create
|
81
|
+
end
|
82
|
+
|
83
|
+
# To enable logging, do this:
|
84
|
+
#
|
85
|
+
# Testable.log_level = :DEBUG
|
86
|
+
# Testable.log_level = 'DEBUG'
|
87
|
+
# Testable.log_level = 0
|
88
|
+
#
|
89
|
+
# This can accept any of a Symbol / String / Integer as an input
|
90
|
+
# To disable all logging, which is the case by default, do this:
|
91
|
+
#
|
92
|
+
# Testable.log_level = :UNKNOWN
|
93
|
+
def log_level=(value)
|
94
|
+
logger.level = value
|
95
|
+
end
|
96
|
+
|
97
|
+
# To query what level is being logged, do this:
|
98
|
+
#
|
99
|
+
# Testable.log_level
|
100
|
+
#
|
101
|
+
# The logging level will be UNKNOWN by default.
|
102
|
+
def log_level
|
103
|
+
%i[DEBUG INFO WARN ERROR FATAL UNKNOWN][logger.level]
|
104
|
+
end
|
105
|
+
|
106
|
+
# The writer method allows you to configure where you want the output of
|
107
|
+
# the Testable logs to go, with the default being standard output. Here
|
108
|
+
# is how you could change this to a specific file:
|
109
|
+
#
|
110
|
+
# Testable.log_path = 'testable.log'
|
111
|
+
def log_path=(logdev)
|
112
|
+
logger.reopen(logdev)
|
113
|
+
end
|
114
|
+
|
115
|
+
# The wire logger provides logging from Watir, which is very similar to the
|
116
|
+
# logging provided by Selenium::WebDriver::Logger. The default level is set
|
117
|
+
# to warn. This means you will see any deprecation notices as well as any
|
118
|
+
# warning messages. To see details on each element interaction the level
|
119
|
+
# can be set to info. To see details on what Watir is doing when it takes a
|
120
|
+
# selector hash and converts it into XPath, the level can be set to debug.
|
121
|
+
# If you want to ignore specific warnings that are appearing during test
|
122
|
+
# execution:
|
123
|
+
#
|
124
|
+
# Watir.logger.ignore :warning_name
|
125
|
+
#
|
126
|
+
# If you want to ignore all deprecation warnings in your tests:
|
127
|
+
#
|
128
|
+
# Watir.logger.ignore :deprecations
|
129
|
+
#
|
130
|
+
# To have the wire logger generate output to a file:
|
131
|
+
#
|
132
|
+
# Watir.logger.output = "wire.log"
|
133
|
+
|
134
|
+
def wire_path=(logdev)
|
135
|
+
Watir.logger.reopen(logdev)
|
136
|
+
end
|
137
|
+
|
138
|
+
def wire_level_logging=(value)
|
139
|
+
Watir.logger.level = value
|
140
|
+
end
|
141
|
+
|
142
|
+
def wire_level_logging
|
143
|
+
%i[DEBUG INFO WARN ERROR FATAL UNKNOWN][Watir.logger.level]
|
144
|
+
end
|
145
|
+
|
146
|
+
def watir_api
|
147
|
+
browser.methods - Object.public_methods -
|
148
|
+
Watir::Container.instance_methods
|
149
|
+
end
|
150
|
+
|
151
|
+
def selenium_api
|
152
|
+
browser.driver.methods - Object.public_methods
|
153
|
+
end
|
154
|
+
|
155
|
+
# This accessor is needed so that Testable itself can provide a browser
|
156
|
+
# reference to indicate connection to WebDriver. This is a class-level
|
157
|
+
# access to the browser.
|
32
158
|
attr_accessor :browser
|
33
159
|
|
34
160
|
def set_browser(app = :chrome, *args)
|
@@ -36,8 +162,14 @@ module Testable
|
|
36
162
|
Testable.browser = @browser
|
37
163
|
end
|
38
164
|
|
165
|
+
alias start_browser set_browser
|
166
|
+
|
39
167
|
def quit_browser
|
40
168
|
@browser.quit
|
41
169
|
end
|
170
|
+
|
171
|
+
def api
|
172
|
+
methods - Object.public_methods
|
173
|
+
end
|
42
174
|
end
|
43
175
|
end
|
@@ -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,82 @@
|
|
1
|
+
module Testable
|
2
|
+
module DSL
|
3
|
+
# The DSL module is mixed into the Node class to provide the DSL for
|
4
|
+
# defining elements and components.
|
5
|
+
def self.included(caller)
|
6
|
+
caller.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
# The ClassMethods provide a set of macro-like methods for wrapping
|
11
|
+
# HTML fragments in Node objects.
|
12
|
+
|
13
|
+
# Defines an element that wraps an HTML fragment.
|
14
|
+
def element(name, selector, options = {})
|
15
|
+
define_method(name.to_s) do
|
16
|
+
Node.new(node: @node.find(selector, options))
|
17
|
+
end
|
18
|
+
|
19
|
+
define_helpers(name, selector)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Defines a collection of elements that wrap HTML fragments.
|
23
|
+
def elements(name, selector, options = {})
|
24
|
+
options = { minimum: 1 }.merge(options)
|
25
|
+
|
26
|
+
define_method(name.to_s) do
|
27
|
+
@node.all(selector, options).map do |node|
|
28
|
+
Node.new(node: node)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
define_helpers(name, selector)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Defines a component that wraps an HTML fragment.
|
36
|
+
def component(name, klass, selector, options = {})
|
37
|
+
unless klass < Node
|
38
|
+
raise ArgumentError, 'Must be given a subclass of Node'
|
39
|
+
end
|
40
|
+
|
41
|
+
define_method(name.to_s) do
|
42
|
+
klass.new(node: @node.find(selector, options))
|
43
|
+
end
|
44
|
+
|
45
|
+
define_helpers(name, selector)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Defines a collection of components that wrap HTML fragments.
|
49
|
+
def components(name, klass, selector, options = {})
|
50
|
+
unless klass < Node
|
51
|
+
raise ArgumentError, 'Must be given a subclass of Node'
|
52
|
+
end
|
53
|
+
|
54
|
+
options = { minimum: 1 }.merge(options)
|
55
|
+
|
56
|
+
define_method(name.to_s) do
|
57
|
+
@node.all(selector, options).map do |node|
|
58
|
+
klass.new(node: node)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
define_helpers(name, selector)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def define_helpers(name, selector)
|
68
|
+
define_existence_predicates(name, selector)
|
69
|
+
end
|
70
|
+
|
71
|
+
def define_existence_predicates(name, selector)
|
72
|
+
define_method("has_#{name}?") do
|
73
|
+
@node.has_selector?(selector)
|
74
|
+
end
|
75
|
+
|
76
|
+
define_method("has_no_#{name}?") do
|
77
|
+
@node.has_no_selector?(selector)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "testable/capybara/dsl"
|
2
|
+
|
3
|
+
module Testable
|
4
|
+
class Node
|
5
|
+
# The Node class represents a wrapped HTML page or fragment. It exposes all
|
6
|
+
# methods of the Cogent DSL, making sure that any Capybara API methods
|
7
|
+
# are passed to the node instance.
|
8
|
+
include DSL
|
9
|
+
|
10
|
+
attr_reader :node
|
11
|
+
|
12
|
+
# A Capybara node is being wrapped in a node instance.
|
13
|
+
def initialize(node:)
|
14
|
+
@node = node
|
15
|
+
end
|
16
|
+
|
17
|
+
# Any Capybara API calls will be sent to the node object.
|
18
|
+
def method_missing(name, *args, &block)
|
19
|
+
if @node.respond_to?(name)
|
20
|
+
@node.send(name, *args, &block)
|
21
|
+
else
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def respond_to_missing?(name, include_private = false)
|
27
|
+
@node.respond_to?(name) || super
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|