browsery 0.1.0 → 0.2.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 +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'
|