selenium_surfer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: