pincers 0.1.0

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