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