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 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'