browsery 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/bin/browsery +5 -0
- data/browsery.gemspec +8 -0
- data/lib/browsery.rb +22 -0
- data/lib/browsery/connector.rb +287 -0
- data/lib/browsery/console.rb +15 -0
- data/lib/browsery/init.rb +60 -0
- data/lib/browsery/logger.rb +12 -0
- data/lib/browsery/page_objects.rb +23 -0
- data/lib/browsery/page_objects/base.rb +266 -0
- data/lib/browsery/page_objects/element_container.rb +50 -0
- data/lib/browsery/page_objects/overlay/base.rb +85 -0
- data/lib/browsery/page_objects/widgets/base.rb +52 -0
- data/lib/browsery/parallel.rb +265 -0
- data/lib/browsery/runner.rb +111 -0
- data/lib/browsery/settings.rb +114 -0
- data/lib/browsery/test_case.rb +266 -0
- data/lib/browsery/test_cases.rb +7 -0
- data/lib/browsery/utils.rb +10 -0
- data/lib/browsery/utils/assertion_helper.rb +35 -0
- data/lib/browsery/utils/castable.rb +103 -0
- data/lib/browsery/utils/data_generator_helper.rb +145 -0
- data/lib/browsery/utils/loggable.rb +16 -0
- data/lib/browsery/utils/overlay_and_widget_helper.rb +78 -0
- data/lib/browsery/utils/page_object_helper.rb +263 -0
- data/lib/browsery/version.rb +1 -1
- data/lib/minitap/minitest5_browsery.rb +22 -0
- data/lib/minitest/autobot_settings_plugin.rb +83 -0
- data/lib/selenium/webdriver/common/element_browsery.rb +21 -0
- data/lib/tapout/custom_reporters/fancy_tap_reporter.rb +94 -0
- data/lib/yard/tagged_test_case_handler.rb +61 -0
- metadata +131 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 85407a347010a7b4f68d5ec210f81a1489cc5362
|
4
|
+
data.tar.gz: 65a5ea4757e1043921bb2b5de62de5266450f412
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8ea9d2d380ae17da3f9901d5f1799c4258a7c14904dd9c919a031b79d12143b2208c237c684456b3630aad58c729a25dc0e762ec62672d10e6b0a556b7f8357e
|
7
|
+
data.tar.gz: 7c9201d74f3d8d1acaa033f20316b1846c58f0bf9978164039555456b8bbee45b4751992701a3429517609bd996e47230a17c4087cdb956567a729fed53bf2a2
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.2.3
|
data/bin/browsery
ADDED
data/browsery.gemspec
CHANGED
@@ -17,6 +17,14 @@ Gem::Specification.new do |s|
|
|
17
17
|
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
18
|
s.require_paths = ["lib"]
|
19
19
|
|
20
|
+
s.add_dependency 'activesupport', '~> 4.2'
|
21
|
+
s.add_dependency 'faker', '~> 1.4'
|
22
|
+
s.add_dependency 'minitap', '~> 0.5.3'
|
23
|
+
s.add_dependency 'pry', '~> 0.10'
|
24
|
+
s.add_dependency 'minitest', '~>5.4.0'
|
25
|
+
s.add_dependency 'selenium-webdriver', '~> 2.46'
|
26
|
+
s.add_dependency 'rest-client', '~> 1.8'
|
27
|
+
|
20
28
|
s.add_development_dependency 'rake'
|
21
29
|
s.add_development_dependency 'yard'
|
22
30
|
end
|
data/lib/browsery.rb
CHANGED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
|
3
|
+
envs = [:default]
|
4
|
+
|
5
|
+
Bundler.setup(*envs)
|
6
|
+
require 'minitest'
|
7
|
+
require 'yaml'
|
8
|
+
require 'erb'
|
9
|
+
require 'faker'
|
10
|
+
require 'selenium/webdriver'
|
11
|
+
require 'rest-client'
|
12
|
+
require 'json'
|
13
|
+
require 'cgi'
|
14
|
+
require 'pathname'
|
15
|
+
require 'active_support/logger'
|
16
|
+
|
17
|
+
require_relative 'minitap/minitest5_browsery'
|
18
|
+
require_relative 'selenium/webdriver/common/element_browsery'
|
19
|
+
|
20
|
+
Time::DATE_FORMATS[:month_day_year] = "%m/%d/%Y"
|
21
|
+
|
22
|
+
require_relative 'browsery/init'
|
@@ -0,0 +1,287 @@
|
|
1
|
+
module Browsery
|
2
|
+
|
3
|
+
# A connector provides a thin layer that combines configuration files and
|
4
|
+
# access to the WebDriver. It's a thin layer in that, other than #initialize,
|
5
|
+
# it is a drop-in replacement for WebDriver calls.
|
6
|
+
#
|
7
|
+
# For example, if you usually access a method as `@driver.find_element`, you
|
8
|
+
# can still access them as the same method under `@connector.find_element`.
|
9
|
+
class Connector
|
10
|
+
|
11
|
+
# Simple configuration container for all profiles. Struct is not used here
|
12
|
+
# because it contaminates the class with Enumerable methods, which will
|
13
|
+
# cause #method_missing in Connector to get confused.
|
14
|
+
class Config
|
15
|
+
attr_accessor :connector, :env
|
16
|
+
|
17
|
+
def ==(other)
|
18
|
+
self.class == other.class && self.connector == other.connector && self.env == other.env
|
19
|
+
end
|
20
|
+
|
21
|
+
alias_method :eql?, :==
|
22
|
+
|
23
|
+
# Hashing mechanism should only look at the connector and environment values
|
24
|
+
def hash
|
25
|
+
@connector.hash ^ @env.hash
|
26
|
+
end
|
27
|
+
|
28
|
+
# Initialize a new configuration object. This object should never be
|
29
|
+
# instantiated directly.
|
30
|
+
#
|
31
|
+
# @api private
|
32
|
+
def initialize(connector, env)
|
33
|
+
@connector = connector
|
34
|
+
@env = env
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
class <<self # :nodoc:
|
40
|
+
protected
|
41
|
+
attr_accessor :finalization_queue
|
42
|
+
end
|
43
|
+
|
44
|
+
self.finalization_queue = Queue.new
|
45
|
+
|
46
|
+
# Finalize connectors in the pool that are no longer used, and then clear
|
47
|
+
# the pool if it should be empty.
|
48
|
+
def self.finalize!(force = false)
|
49
|
+
return if Browsery.settings.reuse_driver? && !force
|
50
|
+
|
51
|
+
if Thread.current[:active_connector]
|
52
|
+
self.finalization_queue << Thread.current[:active_connector]
|
53
|
+
Thread.current[:active_connector] = nil
|
54
|
+
end
|
55
|
+
|
56
|
+
return unless Browsery.settings.auto_finalize?
|
57
|
+
|
58
|
+
while self.finalization_queue.size > 0
|
59
|
+
connector = self.finalization_queue.pop
|
60
|
+
begin
|
61
|
+
connector.finalize!
|
62
|
+
rescue => e
|
63
|
+
Browsery.logger.error("Could not finalize Connector(##{connector.object_id}): #{e.message}")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Given a connector profile and an environment profile, this method will
|
69
|
+
# instantiate a connector object with the correct WebDriver instance and
|
70
|
+
# settings.
|
71
|
+
#
|
72
|
+
# @raise ArgumentError
|
73
|
+
# @param connector [#to_s] the name of the connector profile to use.
|
74
|
+
# @param env [#to_s] the name of the environment profile to use.
|
75
|
+
# @return [Connector] an initialized connector object
|
76
|
+
def self.get(connector_id, env_id)
|
77
|
+
# Ensure arguments are at least provided
|
78
|
+
raise ArgumentError, "A connector must be provided" if connector_id.blank?
|
79
|
+
raise ArgumentError, "An environment must be provided" if env_id.blank?
|
80
|
+
|
81
|
+
# Find the connector and environment profiles
|
82
|
+
connector_cfg = self.load(Browsery.root.join('config/browsery', 'connectors'), connector_id)
|
83
|
+
env_cfg = self.load(Browsery.root.join('config/browsery', 'environments'), env_id)
|
84
|
+
cfg = Config.new(connector_cfg, env_cfg)
|
85
|
+
|
86
|
+
if Thread.current[:active_connector] && !Browsery.settings.reuse_driver?
|
87
|
+
self.finalization_queue << Thread.current[:active_connector]
|
88
|
+
Thread.current[:active_connector] = nil
|
89
|
+
end
|
90
|
+
|
91
|
+
# If the current thread already has an active connector, and the connector
|
92
|
+
# is of the same type requested, reuse it after calling `reset!`
|
93
|
+
active_connector = Thread.current[:active_connector]
|
94
|
+
if active_connector.present?
|
95
|
+
if active_connector.config == cfg
|
96
|
+
active_connector.reset!
|
97
|
+
else
|
98
|
+
self.finalization_queue << active_connector
|
99
|
+
active_connector = nil
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Reuse or instantiate
|
104
|
+
Thread.current[:active_connector] = active_connector || Connector.new(cfg)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Retrieve the default connector for the current environment.
|
108
|
+
#
|
109
|
+
# @raise ArgumentError
|
110
|
+
# @return [Connector] an initialized connector object
|
111
|
+
def self.get_default
|
112
|
+
connector = Browsery.settings.connector
|
113
|
+
env = Browsery.settings.env
|
114
|
+
Browsery.logger.debug("Retrieving connector with settings (#{connector}, #{env})")
|
115
|
+
|
116
|
+
# Get a connector instance and use it in the new page object
|
117
|
+
self.get(connector, env)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Equivalent to @driver.browser
|
121
|
+
def self.browser_name
|
122
|
+
Thread.current[:active_connector].browser
|
123
|
+
end
|
124
|
+
|
125
|
+
# Load profile from a specific path using the selector(s) specified.
|
126
|
+
#
|
127
|
+
# @raise ArgumentError
|
128
|
+
# @param path [#to_path, #to_s] the path in which to find the profile
|
129
|
+
# @param selector [String] semicolon-delimited selector set
|
130
|
+
# @return [Hash] immutable configuration values
|
131
|
+
def self.load(path, selector)
|
132
|
+
overrides = selector.to_s.split(/:/)
|
133
|
+
name = overrides.shift
|
134
|
+
filepath = path.join("#{name}.yml")
|
135
|
+
raise ArgumentError, "Cannot load profile #{name.inspect} because #{filepath.inspect} does not exist" unless filepath.exist?
|
136
|
+
|
137
|
+
cfg = YAML.load(ERB.new(File.read(filepath)).result)
|
138
|
+
cfg = self.resolve(cfg, overrides)
|
139
|
+
cfg.freeze
|
140
|
+
end
|
141
|
+
|
142
|
+
# Resolve a set of profile overrides.
|
143
|
+
#
|
144
|
+
# @param cfg [Hash] the configuration structure optionally containing a
|
145
|
+
# key of `:overrides`
|
146
|
+
# @param overrides [Enumerable<String>]
|
147
|
+
# @return [Hash] the resolved configuration
|
148
|
+
def self.resolve(cfg, overrides)
|
149
|
+
cfg = cfg.dup.with_indifferent_access
|
150
|
+
|
151
|
+
if options = cfg.delete(:overrides)
|
152
|
+
# Evaluate each override in turn, allowing each override to--well,
|
153
|
+
# override--anything coming before it
|
154
|
+
overrides.each do |override|
|
155
|
+
if tree = options[override]
|
156
|
+
cfg.deep_merge!(tree)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
cfg
|
162
|
+
end
|
163
|
+
|
164
|
+
attr_reader :config
|
165
|
+
|
166
|
+
# Perform cleanup on the connector and driver.
|
167
|
+
def finalize!
|
168
|
+
@driver.quit
|
169
|
+
true
|
170
|
+
end
|
171
|
+
|
172
|
+
# Initialize a new connector with a set of configuration files.
|
173
|
+
#
|
174
|
+
# @see Connector.get
|
175
|
+
# @api private
|
176
|
+
def initialize(config)
|
177
|
+
@config = config
|
178
|
+
|
179
|
+
# Load and configure the WebDriver, if necessary
|
180
|
+
if concon = config.connector
|
181
|
+
driver_config = { }
|
182
|
+
driver = concon[:driver]
|
183
|
+
raise ArgumentError, "Connector driver must not be empty" if driver.nil?
|
184
|
+
|
185
|
+
# Handle hub-related options, like hub URLs (for remote execution)
|
186
|
+
if hub = concon[:hub]
|
187
|
+
builder = URI.parse(hub[:url])
|
188
|
+
builder.user = hub[:user] if hub.has_key?(:user)
|
189
|
+
builder.password = hub[:pass] if hub.has_key?(:pass)
|
190
|
+
|
191
|
+
Browsery.logger.debug("Connector(##{self.object_id}): targeting remote #{builder.to_s}")
|
192
|
+
driver_config[:url] = builder.to_s
|
193
|
+
end
|
194
|
+
|
195
|
+
# Handle driver-related timeouts
|
196
|
+
if timeouts = concon[:timeouts]
|
197
|
+
client = Selenium::WebDriver::Remote::Http::Default.new
|
198
|
+
client.timeout = timeouts[:driver]
|
199
|
+
driver_config[:http_client] = client
|
200
|
+
end
|
201
|
+
|
202
|
+
# Handle archetypal capability lists
|
203
|
+
if archetype = concon[:archetype]
|
204
|
+
Browsery.logger.debug("Connector(##{self.object_id}): using #{archetype.inspect} as capabilities archetype")
|
205
|
+
caps = Selenium::WebDriver::Remote::Capabilities.send(archetype)
|
206
|
+
if caps_set = concon[:capabilities]
|
207
|
+
caps.merge!(caps_set)
|
208
|
+
end
|
209
|
+
driver_config[:desired_capabilities] = caps
|
210
|
+
end
|
211
|
+
|
212
|
+
# Load Firefox profile if specified - applicable only when using the firefoxdriver
|
213
|
+
if profile = concon[:profile]
|
214
|
+
driver_config[:profile] = profile
|
215
|
+
end
|
216
|
+
|
217
|
+
# Initialize the driver and declare explicit browser timeouts
|
218
|
+
Browsery.logger.debug("Connector(##{self.object_id}): using WebDriver(#{driver.inspect}, #{driver_config.inspect})")
|
219
|
+
@driver = Selenium::WebDriver.for(driver.to_sym, driver_config)
|
220
|
+
|
221
|
+
# Resize browser window for local browser with 'resolution'
|
222
|
+
if concon[:resolution]
|
223
|
+
width = concon[:resolution].split(/x/)[0].to_i
|
224
|
+
height = concon[:resolution].split(/x/)[1].to_i
|
225
|
+
@driver.manage.window.resize_to(width, height)
|
226
|
+
end
|
227
|
+
|
228
|
+
# setTimeout is undefined for safari driver so skip these steps for it
|
229
|
+
unless @driver.browser == :safari
|
230
|
+
if timeouts = concon[:timeouts]
|
231
|
+
@driver.manage.timeouts.implicit_wait = timeouts[:implicit_wait] if timeouts[:implicit_wait]
|
232
|
+
@driver.manage.timeouts.page_load = timeouts[:page_load] if timeouts[:page_load]
|
233
|
+
@driver.manage.timeouts.script_timeout = timeouts[:script_timeout] if timeouts[:script_timeout]
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# Forward any other method call to the configuration container; if that
|
240
|
+
# fails, forward it to the WebDriver. The WebDriver will take care of any
|
241
|
+
# method resolution errors.
|
242
|
+
#
|
243
|
+
# @param name [#to_sym] symbol representing the method call
|
244
|
+
# @param args [*Object] arguments to be passed along
|
245
|
+
def method_missing(name, *args, &block)
|
246
|
+
if @config.respond_to?(name)
|
247
|
+
@config.send(name, *args, *block)
|
248
|
+
else
|
249
|
+
Browsery.logger.debug("Connector(##{self.object_id})->#{name}(#{args.map { |a| a.inspect }.join(', ')})")
|
250
|
+
@driver.send(name, *args, &block)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
# Resets the current session by deleting all cookies and clearing all local
|
255
|
+
# and session storage. Local and session storage are only cleared if the
|
256
|
+
# underlying driver supports it, and even then, only if the storage
|
257
|
+
# supports atomic clearing.
|
258
|
+
#
|
259
|
+
# @return [Boolean]
|
260
|
+
def reset!
|
261
|
+
@driver.manage.delete_all_cookies
|
262
|
+
@driver.try(:local_storage).try(:clear)
|
263
|
+
@driver.try(:session_storage).try(:clear)
|
264
|
+
true
|
265
|
+
end
|
266
|
+
|
267
|
+
# Forward unhandled message checks to the configuration and driver.
|
268
|
+
#
|
269
|
+
# @param name [#to_sym]
|
270
|
+
# @return [Boolean]
|
271
|
+
def respond_to?(name)
|
272
|
+
super || @config.respond_to?(name) || @driver.respond_to?(name)
|
273
|
+
end
|
274
|
+
|
275
|
+
# Compose a URL from the provided +path+ and the environment profile. The
|
276
|
+
# latter contains things like the hostname, port, SSL settings.
|
277
|
+
#
|
278
|
+
# @param path [#to_s] the path to append after the root URL.
|
279
|
+
# @return [URI] the composed URL.
|
280
|
+
def url_for(path)
|
281
|
+
root = @config.env[:root]
|
282
|
+
raise ArgumentError, "The 'root' attribute is missing from the environment profile" unless root
|
283
|
+
URI.join(root, path)
|
284
|
+
end
|
285
|
+
|
286
|
+
end
|
287
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Browsery
|
2
|
+
class Console < TestCase
|
3
|
+
|
4
|
+
def self.bootstrap!
|
5
|
+
Browsery.settings.tags << [:__dummy__]
|
6
|
+
end
|
7
|
+
|
8
|
+
test :dummy, tags: [:__dummy__, :non_regression], serial: 'DUMMY' do
|
9
|
+
require 'pry'
|
10
|
+
assert_respond_to binding, :pry
|
11
|
+
binding.pry
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# The base module for everything Browsery and is the container for other
|
2
|
+
# modules and classes in the hierarchy:
|
3
|
+
#
|
4
|
+
# * `Connector` provides support for drivers and connector profiles;
|
5
|
+
# * `PageObjects` provides a hierarchy of page objects, page modules, widgets,
|
6
|
+
# and overlays;
|
7
|
+
# * `Settings` provides support for internal Browsery settings; and
|
8
|
+
# * `Utils` provides an overarching module for miscellaneous helper modules.
|
9
|
+
module Browsery
|
10
|
+
|
11
|
+
def self.logger
|
12
|
+
@@logger ||= Browsery::Logger.new($stdout)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.logger=(value)
|
16
|
+
@@logger = value
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.settings
|
20
|
+
@@settings ||= Settings.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.settings=(options)
|
24
|
+
self.settings.merge!(options)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Root directory of the automation repository.
|
28
|
+
# Automation repo can use it to refer to files within itself,
|
29
|
+
# and this gem also uses it to refer to config files of automation,
|
30
|
+
# for example:
|
31
|
+
#
|
32
|
+
# File.read(Browsery.root.join('config/browsery', 'data.yml'))
|
33
|
+
#
|
34
|
+
# will return the contents of `automation_root/config/browsery/data.yml`.
|
35
|
+
#
|
36
|
+
# @return [Pathname] A reference to the root directory, ready to be used
|
37
|
+
# in directory and file path calculations.
|
38
|
+
def self.root
|
39
|
+
@@__root__ ||= Pathname.new(File.expand_path('.'))
|
40
|
+
end
|
41
|
+
|
42
|
+
# Absolute path of root directory of this gem
|
43
|
+
# can be used both within this gem and in automation repo
|
44
|
+
def self.gem_root
|
45
|
+
@@__gem_root__ ||= Pathname.new(File.realpath(File.join(File.dirname(__FILE__), '..', '..')))
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
require_relative 'runner'
|
51
|
+
require_relative 'logger'
|
52
|
+
require_relative 'utils'
|
53
|
+
|
54
|
+
require_relative 'connector'
|
55
|
+
require_relative 'page_objects'
|
56
|
+
require_relative 'parallel'
|
57
|
+
require_relative 'settings'
|
58
|
+
|
59
|
+
require_relative 'test_case'
|
60
|
+
require_relative 'console'
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Browsery
|
2
|
+
class Logger < ActiveSupport::Logger
|
3
|
+
|
4
|
+
LOG_FILE_MODE = File::WRONLY | File::APPEND | File::CREAT
|
5
|
+
|
6
|
+
def initialize(file, *args)
|
7
|
+
file = File.open(Browsery.root.join('logs', file), LOG_FILE_MODE) unless file.respond_to?(:write)
|
8
|
+
super(file, *args)
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Browsery
|
2
|
+
|
3
|
+
# This is the overarching module that contains page objects, modules, and
|
4
|
+
# widgets.
|
5
|
+
#
|
6
|
+
# When new modules or classes are added, an `autoload` clause must be added
|
7
|
+
# into this module so that requires are taken care of automatically.
|
8
|
+
module PageObjects
|
9
|
+
|
10
|
+
# Exception to capture validation problems when instantiating a new page
|
11
|
+
# object. The message contains the page object being instantiated as well
|
12
|
+
# as the original, underlying error message if any.
|
13
|
+
class InvalidePageState < Exception; end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
# Major classes and modules
|
20
|
+
require_relative 'page_objects/element_container'
|
21
|
+
require_relative 'page_objects/base'
|
22
|
+
require_relative 'page_objects/overlay/base'
|
23
|
+
require_relative 'page_objects/widgets/base'
|