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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3b97ab89ccad6c43965274d2c5efe2c4bb84aa3d
4
- data.tar.gz: e1bdef1b837d8fc596d12f5e3f78f004c42e7f12
3
+ metadata.gz: 85407a347010a7b4f68d5ec210f81a1489cc5362
4
+ data.tar.gz: 65a5ea4757e1043921bb2b5de62de5266450f412
5
5
  SHA512:
6
- metadata.gz: 51bf6b17cdc2618a43b89ee78b657b79b320e5fe2359a0978c92cd8cb9fe05221f9a7f130e2bf9072c5a7571b2a28277446801f6a594914c675e73320e6d7ba1
7
- data.tar.gz: fe06da52aff275f304086bca76ac28858bc5e2fe79d9efdcf36c7ded0545b7a3e26f234467123127fdb8c1ae0eb6f517216ca87081bc2ea64a545ae6ff813795
6
+ metadata.gz: 8ea9d2d380ae17da3f9901d5f1799c4258a7c14904dd9c919a031b79d12143b2208c237c684456b3630aad58c729a25dc0e762ec62672d10e6b0a556b7f8357e
7
+ data.tar.gz: 7c9201d74f3d8d1acaa033f20316b1846c58f0bf9978164039555456b8bbee45b4751992701a3429517609bd996e47230a17c4087cdb956567a729fed53bf2a2
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.2.3
data/bin/browsery ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ require 'browsery'
3
+
4
+ Browsery::Runner.after_run { Browsery::Connector.finalize!(:force) }
5
+ Browsery::Runner.run!(ARGV)
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'