selenium_surfer 0.0.1

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.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in selenium_surfer.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Ignacio Baixas
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # SeleniumSurfer
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'selenium_surfer'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install selenium_surfer
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,78 @@
1
+ module SeleniumSurfer
2
+
3
+ # ### Webdriver connection wrapper
4
+ #
5
+ # By wrapping the connection is posible to control reconnection and bound context,
6
+ # this allows for safe context navigation.
7
+ #
8
+ class DriverBucket
9
+
10
+ attr_reader :session_id
11
+
12
+ def initialize(_session_id, _anonymous)
13
+ @session_id = _session_id
14
+ @bound_ctx = nil
15
+ @anonymous = _anonymous
16
+ end
17
+
18
+ # get the current driver instance, reset it if required
19
+ def driver(_reset=false)
20
+ reset if _reset
21
+
22
+ # TODO retrieve config data from config file instead of ENV
23
+
24
+ if @driver.nil?
25
+ driver_name = SeleniumSurfer.config[:webdriver]
26
+ raise ConfigurationError.new 'must provide a webdriver type' if driver_name.nil?
27
+
28
+ @driver = case driver_name.to_sym
29
+ when 'remote'
30
+ url = SeleniumSurfer.config[:remote_host]
31
+
32
+ # setup a custom client to use longer timeouts
33
+ client = Selenium::WebDriver::Remote::Http::Default.new
34
+ client.timeout = SeleniumSurfer.config[:remote_timeout]
35
+
36
+ @driver = Selenium::WebDriver.for :remote, :url => url, :http_client => client
37
+ else
38
+ @driver = Selenium::WebDriver.for driver_name.to_sym
39
+ end
40
+ end
41
+
42
+ return @driver
43
+ end
44
+
45
+ # force current driver connection to be discarded
46
+ def reset
47
+ if @driver
48
+ @driver.quit rescue nil
49
+ @driver = nil
50
+ end
51
+ end
52
+
53
+ # return true if there is a context bound to this bucket
54
+ def bound?
55
+ not @bound_ctx.nil?
56
+ end
57
+
58
+ # bind a context to this bucket
59
+ #
60
+ # The context may implement the `on_unbind` method to be notified when
61
+ # the bucket it is unbound from the bucket
62
+ #
63
+ def bind(_ctx)
64
+ @bound_ctx.on_unbind if @bound_ctx and @bound_ctx.respond_to? :on_unbind
65
+ @bound_ctx = _ctx
66
+ end
67
+
68
+ # unbinds the currently bound context.
69
+ def unbind(_force_reset=false)
70
+ if @bound_ctx
71
+ @bound_ctx.on_unbind if @bound_ctx.respond_to? :on_unbind
72
+ @bound_ctx = nil
73
+ end
74
+ reset if _force_reset or @anonymous # reset bucket if required
75
+ end
76
+ end
77
+
78
+ end
@@ -0,0 +1,15 @@
1
+ module SeleniumSurfer
2
+
3
+ # Error thrown when a bad configuration parameter is passed or missing
4
+ class ConfigurationError < StandardError; end
5
+
6
+ # Error thrown when a driver operation is attempted in an unbound context
7
+ class UnboundContextError < StandardError; end
8
+
9
+ # Error thrown when a programming setup error is found
10
+ class SetupError < StandardError; end
11
+
12
+ # Error thrown when an element operation is attempted in an empty search result set
13
+ class EmptySetError < StandardError; end
14
+
15
+ end
@@ -0,0 +1,104 @@
1
+ module SeleniumSurfer
2
+
3
+ # ### Base class for selenium surfer robots
4
+ #
5
+ # This class defines the interface every test engine must implement.
6
+ #
7
+ # It also provides webdriver managed shared access, this allows for safe
8
+ # webdriver session persistance between tests.
9
+ #
10
+ # Usage: TODO
11
+ #
12
+ class Robot
13
+
14
+ @@all_buckets = {}
15
+ @@loaded_buckets = nil
16
+ @@block_options = nil
17
+
18
+ # execute a block with managed webdriver connections
19
+ #
20
+ # by putting all web related logic inside a managed block the
21
+ # user can rest asured that any unhandled exception inside the block will
22
+ # discard the requested webdrivers connections. This means that future
23
+ # calls to connect will always return valid driver connections.
24
+ #
25
+ def self.managed(_opt={})
26
+
27
+ raise SetupError.new 'cannot call managed block inside managed block' unless @@loaded_buckets.nil?
28
+
29
+ keep_sessions = _opt.fetch(:keep_sessions, false)
30
+ session_error = false
31
+
32
+ # TODO: When `keep_sessions` is used, special care should be taken in preventing a large number
33
+ # of sessions to remain in memory (like in de case a new session id for each run)
34
+
35
+ unless keep_sessions
36
+ # use separate bucket collection if sessions are to be discarded
37
+ temp = @@all_buckets
38
+ @all_buckets = {}
39
+ end
40
+
41
+ @@loaded_buckets = []
42
+ @@block_options = _opt
43
+
44
+ begin
45
+ return yield
46
+ rescue
47
+ session_error = true
48
+ raise
49
+ ensure
50
+ force_reset = session_error or !keep_sessions
51
+ @@loaded_buckets.each { |b| b.unbind(force_reset) }
52
+ @@loaded_buckets = nil
53
+ @all_buckets = temp unless keep_sessions
54
+ end
55
+ end
56
+
57
+ # creates a new surf context and passes it to the given block.
58
+ #
59
+ # * Can only be called inside a `managed` block.
60
+ # * The context is released when block exits.
61
+ #
62
+ def self.new_surf_session(_session=nil, _opt={}, &_block)
63
+
64
+ # load context class to be used, must be a SurfContext or SurfContext subclass
65
+ ctx_class = _opt.fetch(:use, SurfContext)
66
+ raise SetupError.new 'invalid context class' unless ctx_class == SurfContext or ctx_class < SurfContext
67
+
68
+ # make sure this is called within a managed block
69
+ raise SetupError.new 'context is not managed' if @@loaded_buckets.nil?
70
+
71
+ if _session.nil? and not @@block_options.fetch(:nil_sessions, false)
72
+ # create an anonymous bucket
73
+ bucket = DriverBucket.new nil, true
74
+ else
75
+ bucket = @@all_buckets[_session]
76
+ bucket = @@all_buckets[_session] = DriverBucket.new _session, false if bucket.nil?
77
+ raise SetupError.new 'session already bound' if bucket.bound?
78
+ end
79
+
80
+ @@loaded_buckets << bucket
81
+ ctx = ctx_class.new bucket
82
+
83
+ # if block is not given, just return context, if given, pass it to block
84
+ # and ensure release
85
+ return ctx unless _block
86
+ begin
87
+ return _block.call ctx
88
+ ensure
89
+ bucket.unbind _opt.fetch(:on_exit, :release) == :discard
90
+ end
91
+ end
92
+
93
+ # Object instance flavor of `self.managed`
94
+ def managed(_opt={}, &_block)
95
+ return self.class.managed _opt, &_block
96
+ end
97
+
98
+ # Object instance flavor of `self.surf`
99
+ def new_surf_session(_session=nil, _opt={}, &_block)
100
+ return self.class.new_surf_session(_session, _opt, &_block)
101
+ end
102
+
103
+ end
104
+ end
@@ -0,0 +1,75 @@
1
+ require 'forwardable'
2
+
3
+ module SeleniumSurfer
4
+
5
+ # ### WebDriver Element wrapper
6
+ #
7
+ # Provides jQuery-like access to elements.
8
+ #
9
+ class SearchContext
10
+ include Enumerable
11
+ extend Forwardable
12
+
13
+ def initialize(_elements)
14
+ @elements = _elements
15
+ end
16
+
17
+ # forward read-only array methods to context
18
+ def_delegators :context, :each, :[], :length, :count, :empty?, :first, :last
19
+
20
+ def explode(&_block)
21
+ return enum_for(__method__) if _block.nil?
22
+ context.each do |el|
23
+ _block.call SearchContext.new([el])
24
+ end
25
+ end
26
+
27
+ def search(_selector=nil, _options={})
28
+ _options[:css] = _selector if _selector
29
+ SearchContext.new search_elements(_options)
30
+ end
31
+
32
+ def fill(_value)
33
+ raise EmptySetError.new if empty?
34
+ context.first.clear
35
+ context.first.send_keys _value
36
+ end
37
+
38
+ # Any methods missing are forwarded to the first element.
39
+ def method_missing(_method, *_args, &_block)
40
+ m = /^(.*)_all$/.match _method.to_s
41
+ if m then
42
+ return [] if empty?
43
+ context.map { |e| e.send(m[1], *_args, &_block) }
44
+ else
45
+ raise EmptySetError.new if empty?
46
+ context.first.send(_method, *_args, &_block)
47
+ end
48
+ end
49
+
50
+ def respond_to?(_method, _include_all=false)
51
+ return true if super
52
+ m = /^.*_all$/.match _method.to_s
53
+ if m then
54
+ return true if empty?
55
+ context.first.respond_to? m[1], _include_all
56
+ else
57
+ raise EmptySetError.new if empty?
58
+ context.first.respond_to? _method, _include_all
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def search_elements(_options)
65
+ context.inject([]) do |r, element|
66
+ r + element.find_elements(_options)
67
+ end
68
+ end
69
+
70
+ def context
71
+ @elements
72
+ end
73
+
74
+ end
75
+ end
@@ -0,0 +1,150 @@
1
+ module SeleniumSurfer
2
+
3
+ # ### Base class for robot contexts
4
+ #
5
+ class SurfContext < SearchContext
6
+
7
+ # add a macro attribute writer to context.
8
+ #
9
+ # A macro attribute persist through context changes.
10
+ #
11
+ def self.macro_attr_writer(*_names)
12
+ _names.each do |name|
13
+ send :define_method, "#{name}=" do |v| @macro[name.to_sym] = v end
14
+ end
15
+ end
16
+
17
+ # add a macro attribute reader to context.
18
+ #
19
+ # A macro attribute persist through context changes.
20
+ #
21
+ def self.macro_attr_reader(*_names)
22
+ _names.each do |name|
23
+ send :define_method, "#{name}" do @macro[name.to_sym] end
24
+ end
25
+ end
26
+
27
+ # add a macro attribute accessor to context.
28
+ #
29
+ # A macro attribute persist through context changes.
30
+ #
31
+ def self.macro_attr_accessor(*_names)
32
+ macro_attr_reader *_names
33
+ macro_attr_writer *_names
34
+ end
35
+
36
+ macro_attr_accessor :max_retries
37
+
38
+ def initialize(_bucket, _macro=nil, _stack=nil)
39
+ @bucket = _bucket
40
+ @macro = _macro || {}
41
+ @stack = _stack || []
42
+
43
+ @bucket.bind self
44
+ end
45
+
46
+ # return true if context is bound
47
+ def bound?
48
+ not @bucket.nil?
49
+ end
50
+
51
+ # switch to another context
52
+ # new context class should be a SurfContext subclass
53
+ def switch_to(_klass=nil)
54
+ raise UnboundContextError.new unless bound?
55
+ _klass.new @bucket, @macro, @stack
56
+ end
57
+
58
+ # ## Helpers
59
+
60
+ # retrieves the current driver being used by this context
61
+ def driver
62
+ load_driver
63
+ end
64
+
65
+ # return the current page title
66
+ def title
67
+ load_driver.title
68
+ end
69
+
70
+ # navigate to a given url (uses the max_retries setting)
71
+ def navigate(_url, _params=nil)
72
+ _url += "?#{_params.to_query}" if _params
73
+ retries = 0
74
+
75
+ loop do
76
+ begin
77
+ load_driver(retries > 0).get(_url)
78
+ @stack = [] # clear stack after successfull navigation
79
+ break
80
+ rescue Timeout::Error, Selenium::WebDriver::Error::UnknownError
81
+ trace "Error when opening #{_url}!"
82
+ raise if retries >= @max_retries
83
+ retries += 1
84
+ sleep 1.0
85
+ end
86
+ end
87
+ end
88
+
89
+ # changes the context
90
+ # TODO: this method may be unecesary...
91
+ def step(_selector=nil, _options={})
92
+ _options[:css] = _selector if _selector
93
+ new_context = search_elements(_options)
94
+ begin
95
+ @stack << new_context
96
+ yield
97
+ ensure
98
+ @stack.pop
99
+ end
100
+
101
+ return true
102
+ end
103
+
104
+ # release current driver connection
105
+ def release
106
+ return false if not bound?
107
+ @bucket.unbind
108
+ return true
109
+ end
110
+
111
+ # release and discard the current driver connection.
112
+ def quit
113
+ return false if not bound?
114
+ @bucket.unbind true
115
+ return true
116
+ end
117
+
118
+ # resets the current driver connection, does not release it.
119
+ def reset
120
+ return false if not bound?
121
+ @bucket.reset
122
+ return true
123
+ end
124
+
125
+ # bucket context interface implementation
126
+ # not to be called directly
127
+ def on_unbind
128
+ @bucket = @stack = nil
129
+ end
130
+
131
+ private
132
+
133
+ def load_driver(_reset=false)
134
+ raise UnboundContextError.new if not bound?
135
+ @bucket.reset if _reset
136
+ @bucket.driver
137
+ end
138
+
139
+ def context
140
+ raise UnboundContextError.new if not bound?
141
+ @stack.last || [load_driver]
142
+ end
143
+
144
+ def observe
145
+ # get current url
146
+ return yield
147
+ # compare url after function call, if changed reset stack
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,3 @@
1
+ module SeleniumSurfer
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,38 @@
1
+ require "selenium_surfer/version"
2
+ require "selenium_surfer/errors"
3
+ require "selenium_surfer/driver_bucket"
4
+ require "selenium_surfer/search_context"
5
+ require "selenium_surfer/surf_context"
6
+ require "selenium_surfer/robot"
7
+
8
+ module SeleniumSurfer
9
+
10
+ # Configuration defaults
11
+ @@config = {
12
+ :webdriver => nil,
13
+ :remote_host => 'http://localhost:8080',
14
+ :remote_timeout => 120
15
+ }
16
+
17
+ # Configure through hash
18
+ def self.configure(_opts = {})
19
+ _opts.each { |k,v| @@config[k.to_sym] = v if @@config.has_key? k.to_sym }
20
+ end
21
+
22
+ # Configure through yaml file
23
+ def self.configure_with(_path_to_yaml_file)
24
+ begin
25
+ config = YAML::load(IO.read(_path_to_yaml_file))
26
+ rescue Errno::ENOENT
27
+ log(:warning, "YAML configuration file couldn't be found. Using defaults."); return
28
+ rescue Psych::SyntaxError
29
+ log(:warning, "YAML configuration file contains invalid syntax. Using defaults."); return
30
+ end
31
+
32
+ configure(config)
33
+ end
34
+
35
+ def self.config
36
+ @@config
37
+ end
38
+ end
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'selenium_surfer/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "selenium_surfer"
8
+ spec.version = SeleniumSurfer::VERSION
9
+ spec.authors = ["Ignacio Baixas"]
10
+ spec.email = ["ignacio@platan.us"]
11
+ spec.description = %q{Selenium Surfer Robots!}
12
+ spec.summary = %q{Base infrastructure for webdriver robots with session managing and rich DSL}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency 'selenium-webdriver', "~> 2.33.0"
22
+ spec.add_development_dependency "bundler", "~> 1.3"
23
+ spec.add_development_dependency "rake"
24
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: selenium_surfer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ignacio Baixas
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-08-09 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: selenium-webdriver
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 2.33.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 2.33.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: bundler
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '1.3'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '1.3'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rake
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description: Selenium Surfer Robots!
63
+ email:
64
+ - ignacio@platan.us
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - .gitignore
70
+ - Gemfile
71
+ - LICENSE.txt
72
+ - README.md
73
+ - Rakefile
74
+ - lib/selenium_surfer.rb
75
+ - lib/selenium_surfer/driver_bucket.rb
76
+ - lib/selenium_surfer/errors.rb
77
+ - lib/selenium_surfer/robot.rb
78
+ - lib/selenium_surfer/search_context.rb
79
+ - lib/selenium_surfer/surf_context.rb
80
+ - lib/selenium_surfer/version.rb
81
+ - selenium_surfer.gemspec
82
+ homepage: ''
83
+ licenses:
84
+ - MIT
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ! '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ none: false
97
+ requirements:
98
+ - - ! '>='
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ requirements: []
102
+ rubyforge_project:
103
+ rubygems_version: 1.8.23
104
+ signing_key:
105
+ specification_version: 3
106
+ summary: Base infrastructure for webdriver robots with session managing and rich DSL
107
+ test_files: []
108
+ has_rdoc: