pincers 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9be2262d6757498e82ec195e8e8e5109226df675
4
+ data.tar.gz: 73e0e4943ec46e3dcbd35a043dda468bbb1e75f2
5
+ SHA512:
6
+ metadata.gz: a7fcc330eb821333f73e1d9f4989084e5d7a0d161703542725ab606a244ac10305f69430bb3522e1c7cc5d8a1746a8bb974f4d9585170aa27b59822980c9afd5
7
+ data.tar.gz: 917e6139161b82a3a817b9fb6deb3d31295e60658e138d9004efcb8712f27f41e691c963fedc200c8de22d12c4d398ef5a0e4f2f1176c9523a68b0605b21ab53
data/lib/pincers.rb ADDED
@@ -0,0 +1,20 @@
1
+ require "forwardable"
2
+ require "pincers/version"
3
+ require "pincers/errors"
4
+ require "pincers/factory"
5
+ require "pincers/support/configuration"
6
+
7
+ module Pincers
8
+ extend Factory
9
+
10
+ @@config = Support::Configuration.new
11
+
12
+ def self.config
13
+ @@config
14
+ end
15
+
16
+ def self.configure
17
+ yield @@config
18
+ end
19
+
20
+ end
@@ -0,0 +1,87 @@
1
+ module Pincers::Backend
2
+
3
+ class Base
4
+
5
+ attr_reader :document
6
+
7
+ def initialize(_document)
8
+ @document = _document
9
+ end
10
+
11
+ def document_root
12
+ ensure_implementation :document_root
13
+ end
14
+
15
+ def document_url
16
+ ensure_implementation :document_url
17
+ end
18
+
19
+ def document_title
20
+ ensure_implementation :document_title
21
+ end
22
+
23
+ def document_source
24
+ ensure_implementation :document_source
25
+ end
26
+
27
+ def fetch_cookies
28
+ ensure_implementation :fetch_cookies
29
+ end
30
+
31
+ def navigate_to(_url)
32
+ ensure_implementation :navigate_to
33
+ end
34
+
35
+ def navigate_forward(_steps)
36
+ ensure_implementation :navigate_forward
37
+ end
38
+
39
+ def navigate_back(_steps)
40
+ ensure_implementation :navigate_back
41
+ end
42
+
43
+ def refresh_document
44
+ ensure_implementation :refresh_document
45
+ end
46
+
47
+ def search_by_css(_element, _selector)
48
+ ensure_implementation :search_by_css
49
+ end
50
+
51
+ def search_by_xpath(_element, _selector)
52
+ ensure_implementation :search_by_xpath
53
+ end
54
+
55
+ def extract_element_text(_element)
56
+ ensure_implementation :extract_element_text
57
+ end
58
+
59
+ def extract_element_html(_element)
60
+ ensure_implementation :extract_element_html
61
+ end
62
+
63
+ def extract_element_attribute(_element, _name)
64
+ ensure_implementation :extract_element_attribute
65
+ end
66
+
67
+ def clear_input(_element)
68
+ ensure_implementation :clear_input
69
+ end
70
+
71
+ def fill_input(_element, _value)
72
+ ensure_implementation :fill_input
73
+ end
74
+
75
+ def load_frame_element(_element)
76
+ ensure_implementation :load_frame_element
77
+ end
78
+
79
+ private
80
+
81
+ def ensure_implementation(_name)
82
+ raise MissingFeatureError.new _name
83
+ end
84
+
85
+ end
86
+
87
+ end
@@ -0,0 +1,29 @@
1
+ require 'pincers/backend/base'
2
+
3
+ module Pincers::Backend
4
+
5
+ class Nokogiri < Base
6
+
7
+ def search_by_css(_element, _selector)
8
+ _element.css _selector
9
+ end
10
+
11
+ def search_by_xpath(_element, _selector)
12
+ _element.xpath _selector
13
+ end
14
+
15
+ def extract_element_text(_element)
16
+ # _element.text
17
+ end
18
+
19
+ def extract_element_html(_element)
20
+ # _element['outerHTML']
21
+ end
22
+
23
+ def extract_element_attribute(_element, _name)
24
+ # _element[_name]
25
+ end
26
+
27
+ end
28
+
29
+ end
@@ -0,0 +1,108 @@
1
+ require 'pincers/backend/base'
2
+
3
+ module Pincers::Backend
4
+
5
+ class Webdriver < Base
6
+
7
+ attr_reader :driver
8
+
9
+ def initialize(_driver)
10
+ super _driver
11
+ @driver = _driver
12
+ end
13
+
14
+ def document_root
15
+ [@driver]
16
+ end
17
+
18
+ def document_url
19
+ @driver.current_url
20
+ end
21
+
22
+ def document_title
23
+ @driver.title
24
+ end
25
+
26
+ def document_source
27
+ @driver.page_source
28
+ end
29
+
30
+ def fetch_cookies
31
+ @driver.manage.all_cookies
32
+ end
33
+
34
+ def navigate_to(_url)
35
+ @driver.get _url
36
+ end
37
+
38
+ def navigate_forward(_steps)
39
+ _steps.times { @driver.navigate.forward }
40
+ end
41
+
42
+ def navigate_back(_steps)
43
+ _steps.times { @driver.navigate.back }
44
+ end
45
+
46
+ def refresh_document
47
+ @driver.navigate.refresh
48
+ end
49
+
50
+ def search_by_css(_element, _selector)
51
+ _element.find_elements css: _selector
52
+ end
53
+
54
+ def search_by_xpath(_element, _selector)
55
+ _element.find_elements xpath: _selector
56
+ end
57
+
58
+ def extract_element_text(_element)
59
+
60
+ _element.text
61
+ end
62
+
63
+ def extract_element_html(_element)
64
+ if _element == @driver then @driver.page_source else _element.attribute('outerHTML') end
65
+ end
66
+
67
+ def extract_element_attribute(_element, _name)
68
+ _element[_name]
69
+ end
70
+
71
+ def clear_input(_element)
72
+ _element.clear
73
+ end
74
+
75
+ def fill_input(_element, _value)
76
+ _element.send_keys _value
77
+ end
78
+
79
+ def load_frame_element(_element)
80
+ driver.switch_to.frame _element
81
+ self
82
+ end
83
+
84
+ # wait contitions
85
+
86
+ def check_present(_elements)
87
+ _elements.length > 0
88
+ end
89
+
90
+ def check_not_present(_elements)
91
+ _elements.length == 0
92
+ end
93
+
94
+ def check_visible(_elements)
95
+ check_present(_elements) and _elements.first.displayed?
96
+ end
97
+
98
+ def check_enabled(_elements)
99
+ check_visible(_elements) and _elements.first.enabled?
100
+ end
101
+
102
+ def check_not_visible(_elements)
103
+ not _elements.any? { |e| e.displayed? }
104
+ end
105
+
106
+ end
107
+
108
+ end
@@ -0,0 +1,72 @@
1
+ require 'pincers/support/cookie_jar'
2
+ require 'pincers/core/search_context'
3
+
4
+ module Pincers::Core
5
+ class RootContext < SearchContext
6
+
7
+ attr_reader :config
8
+
9
+ def initialize(_backend, _config={})
10
+ super _backend.document_root, nil
11
+ @backend = _backend
12
+ @config = Pincers.config.values.merge _config
13
+ end
14
+
15
+ def root
16
+ self
17
+ end
18
+
19
+ def backend
20
+ @backend
21
+ end
22
+
23
+ def url
24
+ wrap_errors { backend.document_url }
25
+ end
26
+
27
+ def uri
28
+ URI.parse url
29
+ end
30
+
31
+ def title
32
+ wrap_errors { backend.document_title }
33
+ end
34
+
35
+ def source
36
+ wrap_errors { backend.document_source }
37
+ end
38
+
39
+ def cookies
40
+ @cookies ||= CookieJar.new backend
41
+ end
42
+
43
+ def goto(_url)
44
+ wrap_errors { backend.navigate_to _url }
45
+ self
46
+ end
47
+
48
+ def forward(_steps=1)
49
+ wrap_errors { backend.navigate_forward _steps }
50
+ self
51
+ end
52
+
53
+ def back(_steps=1)
54
+ wrap_errors { backend.navigate_back _steps }
55
+ self
56
+ end
57
+
58
+ def refresh
59
+ wrap_errors { backend.refresh_document _steps }
60
+ self
61
+ end
62
+
63
+ def default_timeout
64
+ @config[:wait_timeout]
65
+ end
66
+
67
+ def default_interval
68
+ @config[:wait_interval]
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,184 @@
1
+ module Pincers::Core
2
+ class SearchContext
3
+ include Enumerable
4
+ extend Forwardable
5
+
6
+ attr_accessor :parent, :elements
7
+
8
+ def_delegators :elements, :length, :count, :empty?
9
+
10
+ def initialize(_elements, _parent)
11
+ @elements = _elements
12
+ @parent = _parent
13
+ end
14
+
15
+ def root
16
+ parent.root
17
+ end
18
+
19
+ def backend
20
+ root.backend
21
+ end
22
+
23
+ def document
24
+ backend.document
25
+ end
26
+
27
+ def element
28
+ elements.first
29
+ end
30
+
31
+ def element!
32
+ raise Pincers::EmptySetError.new self if empty?
33
+ element
34
+ end
35
+
36
+ def each
37
+ elements.each { |el| yield wrap_elements [el] }
38
+ end
39
+
40
+ def [](*args)
41
+ if args[0].is_a? String or args[0].is_a? Symbol
42
+ wrap_errors do
43
+ backend.extract_element_attribute element!, args[0]
44
+ end
45
+ else
46
+ wrap_elements Array(elements.send(:[],*args))
47
+ end
48
+ end
49
+
50
+ def first
51
+ if elements.first.nil? then nil else wrap_elements [elements.first] end
52
+ end
53
+
54
+ def last
55
+ if elements.last.nil? then nil else wrap_elements [elements.last] end
56
+ end
57
+
58
+ def css(_selector, _options={})
59
+ search_with_options _options do
60
+ explode_elements { |e| backend.search_by_css e, _selector }
61
+ end
62
+ end
63
+
64
+ def xpath(_selector, _options={})
65
+ search_with_options _options do
66
+ explode_elements { |e| backend.search_by_xpath e, _selector }
67
+ end
68
+ end
69
+
70
+ def text
71
+ wrap_errors do
72
+ backend.extract_element_text element!
73
+ end
74
+ end
75
+
76
+ def classes
77
+ wrap_errors do
78
+ class_attr = backend.extract_element_attribute element!, 'class'
79
+ (class_attr || '').split(' ')
80
+ end
81
+ end
82
+
83
+ def to_html
84
+ wrap_errors do
85
+ elements.map { |e| backend.extract_element_html e }.join
86
+ end
87
+ end
88
+
89
+ # Input related
90
+
91
+ def set(_value)
92
+ wrap_errors do
93
+ backend.clear_input element!
94
+ backend.fill_input element!
95
+ end
96
+ end
97
+
98
+ def select(_value)
99
+ # TODO.
100
+ end
101
+
102
+ # context related
103
+
104
+ def enter
105
+ wrap_errors do
106
+ RootContext.new backend.load_frame_element(element!), root.config
107
+ end
108
+ end
109
+
110
+ # Any methods missing are forwarded to the main element (first)
111
+
112
+ def method_missing(_method, *_args, &_block)
113
+ wrap_errors do
114
+ m = /^(.*)_all$/.match _method.to_s
115
+ if m then
116
+ return [] if empty?
117
+ elements.map { |e| e.send(m[1], *_args, &_block) }
118
+ else
119
+ element!.send(_method, *_args, &_block)
120
+ end
121
+ end
122
+ end
123
+
124
+ def respond_to?(_method, _include_all=false)
125
+ return true if super
126
+ m = /^.*_all$/.match _method.to_s
127
+ if m then
128
+ return true if empty?
129
+ elements.first.respond_to? m[1], _include_all
130
+ else
131
+ return true if empty?
132
+ elements.first.respond_to? _method, _include_all
133
+ end
134
+ end
135
+
136
+ private
137
+
138
+ def wrap_errors
139
+ begin
140
+ yield
141
+ rescue Pincers::Error
142
+ raise
143
+ rescue Exception => exc
144
+ raise Pincers::BackendError.new self, exc
145
+ end
146
+ end
147
+
148
+ def wrap_elements(_elements)
149
+ SearchContext.new _elements, self
150
+ end
151
+
152
+ def search_with_options(_options, &_block)
153
+ wrap_errors do
154
+ wait_for = _options.delete(:wait)
155
+ return wrap_elements _block.call unless wait_for
156
+ wrap_elements poll_until(wait_for, _options, &_block)
157
+ end
158
+ end
159
+
160
+ def explode_elements
161
+ elements.inject([]) do |r, element|
162
+ r + yield(element)
163
+ end
164
+ end
165
+
166
+ def poll_until(_condition, _options, &_search)
167
+ check_method = "check_#{_condition}"
168
+ raise Pincers::MissingFeatureError.new check_method unless backend.respond_to? check_method
169
+
170
+ timeout = _options.fetch :timeout, root.default_timeout
171
+ interval = _options.fetch :interval, root.default_interval
172
+ end_time = Time.now + timeout
173
+
174
+ until Time.now > end_time
175
+ new_elements = _search.call
176
+ return new_elements if backend.send check_method, new_elements
177
+ sleep interval
178
+ end
179
+
180
+ raise Pincers::ConditionTimeoutError.new self, _condition
181
+ end
182
+
183
+ end
184
+ end
@@ -0,0 +1,60 @@
1
+ module Pincers
2
+
3
+ class Error < StandardError; end
4
+
5
+ class ConfigurationError < Error; end
6
+
7
+ class MissingFeatureError < Error
8
+
9
+ attr_reader :feature
10
+
11
+ def initialize(_feature)
12
+ @feature = _feature
13
+ super "This backend does not provide '#{_feature}'"
14
+ end
15
+
16
+ end
17
+
18
+ class ContextError < Error
19
+
20
+ attr_reader :context
21
+
22
+ def initialize(_context, _msg)
23
+ super _msg
24
+ @context = _context
25
+ end
26
+
27
+ end
28
+
29
+ class EmptySetError < ContextError
30
+
31
+ def initialize(_context)
32
+ super _context, "This set is empty"
33
+ end
34
+
35
+ end
36
+
37
+ class ConditionTimeoutError < ContextError
38
+
39
+ def initialize(_context, _condition)
40
+ super _context, "Timed out waiting element to be #{_condition}"
41
+ end
42
+
43
+ end
44
+
45
+ class BackendError < ContextError
46
+
47
+ attr_reader :document
48
+ attr_reader :original
49
+
50
+ def initialize(_context, _exc)
51
+ super _context, "Backend error: #{_exc.message}"
52
+ @document = _context.document
53
+ @original = _exc
54
+ end
55
+
56
+ # IDEA: join backtraces?
57
+
58
+ end
59
+
60
+ end
@@ -0,0 +1,33 @@
1
+ require 'pincers/core/root_context'
2
+
3
+ module Pincers
4
+ module Factory
5
+
6
+ def for_webdriver(_driver, _options={})
7
+ require 'pincers/backend/webdriver'
8
+
9
+ unless _driver.is_a? Selenium::WebDriver::Driver
10
+ _driver = Selenium::WebDriver::Driver.for _driver, _options
11
+ end
12
+
13
+ context Backend::Webdriver.new _driver
14
+ end
15
+
16
+ def for_nokogiri(_document, _options={})
17
+ require 'pincers/backend/nokogiri'
18
+
19
+ unless _document.is_a? ::Nokogiri::HTML::Document
20
+ _document = ::Nokogiri::HTML _document, _options[:url], _options[:encoding], _options[:flags]
21
+ end
22
+
23
+ context Backend::Nokogiri.new _document
24
+ end
25
+
26
+ private
27
+
28
+ def context(_backend)
29
+ Core::RootContext.new _backend
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,35 @@
1
+ module Pincers::Support
2
+ class Configuration
3
+
4
+ class Option < Struct.new(:name, :type, :text); end
5
+
6
+ FIELDS = [
7
+ [:wait_timeout, 10.0],
8
+ [:wait_interval, 0.2]
9
+ ];
10
+
11
+ FIELDS.each do |field|
12
+ define_method "#{field[0]}=" do |val|
13
+ @values[field[0]] = val
14
+ end
15
+
16
+ define_method "#{field[0]}" do
17
+ @values[field[0]]
18
+ end
19
+ end
20
+
21
+ attr_reader :values
22
+
23
+ def initialize
24
+ reset
25
+ end
26
+
27
+ def set(_options)
28
+ @values.merge! _options
29
+ end
30
+
31
+ def reset
32
+ @values = Hash[FIELDS]
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,16 @@
1
+ require 'ostruct'
2
+
3
+ module Pincers::Support
4
+ class CookieJar
5
+ include Enumerable
6
+
7
+ def initialize(_backend)
8
+ @backend = _backend
9
+ end
10
+
11
+ def each
12
+ @backend.fetch_cookies.each { |c| yield OpenStruct.new c }
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ module Pincers
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,224 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pincers
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ignacio Baixas
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-08-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: selenium-webdriver
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '2.45'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '2.45'
27
+ - !ruby/object:Gem::Dependency
28
+ name: nokogiri
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 1.6.6
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: 1.6.6
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '1.6'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '1.6'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec-nc
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: guard
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: guard-rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: terminal-notifier-guard
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ~>
130
+ - !ruby/object:Gem::Version
131
+ version: 1.6.1
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ~>
137
+ - !ruby/object:Gem::Version
138
+ version: 1.6.1
139
+ - !ruby/object:Gem::Dependency
140
+ name: pry
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - '>='
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - '>='
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: pry-remote
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - '>='
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - '>='
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: pry-nav
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - '>='
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - '>='
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ description:
182
+ email:
183
+ - ignacio@platan.us
184
+ executables: []
185
+ extensions: []
186
+ extra_rdoc_files: []
187
+ files:
188
+ - lib/pincers/backend/base.rb
189
+ - lib/pincers/backend/nokogiri.rb
190
+ - lib/pincers/backend/webdriver.rb
191
+ - lib/pincers/core/root_context.rb
192
+ - lib/pincers/core/search_context.rb
193
+ - lib/pincers/errors.rb
194
+ - lib/pincers/factory.rb
195
+ - lib/pincers/support/configuration.rb
196
+ - lib/pincers/support/cookie_jar.rb
197
+ - lib/pincers/version.rb
198
+ - lib/pincers.rb
199
+ homepage: https://github.com/platanus/pincers
200
+ licenses:
201
+ - MIT
202
+ metadata: {}
203
+ post_install_message:
204
+ rdoc_options: []
205
+ require_paths:
206
+ - lib
207
+ required_ruby_version: !ruby/object:Gem::Requirement
208
+ requirements:
209
+ - - '>='
210
+ - !ruby/object:Gem::Version
211
+ version: '0'
212
+ required_rubygems_version: !ruby/object:Gem::Requirement
213
+ requirements:
214
+ - - '>='
215
+ - !ruby/object:Gem::Version
216
+ version: '0'
217
+ requirements: []
218
+ rubyforge_project:
219
+ rubygems_version: 2.0.14
220
+ signing_key:
221
+ specification_version: 4
222
+ summary: Web automation framework with multiple backend support
223
+ test_files: []
224
+ has_rdoc: