web_minion 0.1.0 → 0.3.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: bf7a988efff88355bcdc557cdf6824061e883bbd
4
- data.tar.gz: 43361685a55294c9d4c21f13d5bd273b749fe5e7
3
+ metadata.gz: 477c3b18a2d94887c6d2e3ad2fa1c59b6d4dfd74
4
+ data.tar.gz: eb231e8060d78d0e59ce36c97cabba543af1a4f1
5
5
  SHA512:
6
- metadata.gz: fdb3412034e560b84953577ecf12f11962c41f402d442df73a81016bf73beee5f133b395f0cdadc85631f93223364d8165b0e2a86737bba9dc63d8096ff0a2f1
7
- data.tar.gz: acfee494e048cd9f65f05ddffc5b381f4a8af92bb6ba9b8163d5daf353a09710323bc20dadfb5e5edd212984d092c7c6b6a7d0b4f7883c5a04d9f1f59b34640c
6
+ metadata.gz: a74a18184389fe2ead049b7ece8629d952d8f61ffa73d40e40be1d30b8433a0205930278afed97df5edda13e472bd6cfed533b0f2151df8dc6b376c41b4880b7
7
+ data.tar.gz: 7df05983de55cc6d37ac372b520f4136ed521e20fd801ac5aec40d9809e7c71b0a2718577c7f3edd8c86e0af5359efa064bf83e6bffa716f468697741bfce821
data/.gitignore CHANGED
@@ -1,3 +1,9 @@
1
+ *.DS_Store
2
+ cl_tester.rb
3
+ completer.hist
4
+ cl_testing_html.html
5
+ *.swo
6
+ *.swp
1
7
  /.bundle/
2
8
  /.yardoc
3
9
  /Gemfile.lock
data/.travis.yml CHANGED
@@ -2,7 +2,7 @@ language: ruby
2
2
  rvm:
3
3
  - 2.3.1
4
4
  - 2.2.5
5
- - 2.1.9
6
- - 2.0.0
7
- - 1.9.3-p551
8
5
  - ruby-head
6
+ matrix:
7
+ allow_failures:
8
+ - rvm: ruby-head
data/Gemfile CHANGED
@@ -1,6 +1,9 @@
1
1
  source "https://rubygems.org"
2
2
 
3
3
  gem "mechanize"
4
+ gem "capybara"
5
+ gem "poltergeist"
6
+ gem "selenium-webdriver"
4
7
 
5
8
  group :development do
6
9
  gem "pry"
data/README.md CHANGED
@@ -1,11 +1,13 @@
1
1
  # WebMinion
2
2
  - [![Build](http://img.shields.io/travis-ci/jibeinc/web_minion.svg?style=flat-square)](https://travis-ci.org/jibeinc/web_minion)
3
3
  - [![Quality](http://img.shields.io/codeclimate/github/jibeinc/web_minion.svg?style=flat-square)](https://codeclimate.com/github/jibeinc/web_minion)
4
- - [![Coveralls](https://img.shields.io/coveralls/jibeinc/web_minion.svg?style=flat-square)](https://img.shields.io/coveralls/jibeinc/web_minion.svg)
4
+ - [![Coveralls](https://img.shields.io/coveralls/jibeinc/web_minion.svg?style=flat-square)](https://coveralls.io/github/jibeinc/web_minion)
5
5
  - [![Issues](http://img.shields.io/github/issues/jibeinc/web_minion.svg?style=flat-square)](http://github.com/jibeinc/web_minion/issues)
6
6
  - [![License](http://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](http://opensource.org/licenses/MIT)
7
7
 
8
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/web_minion`. To experiment with that code, run `bin/console` for an interactive prompt.
8
+ WebMinion is a metadata-driven browser automation library. Instead of writing a custom bot with lots of code, you can write a JSON configuration and give it to WebMinion to run instead. You can use webdrivers like Mechanize, Capybara/Selenium (TODO) and Capybara/PhantomJS (TODO).
9
+
10
+ *NOTE* The public API is currently unstable and subject to change.
9
11
 
10
12
  ## Installation
11
13
 
@@ -25,12 +27,19 @@ Or install it yourself as:
25
27
 
26
28
  ## Usage
27
29
 
30
+ ```ruby
31
+ web_minion = WebMinion::Flow.build_via_json(File.read("./test/test_json/test_json_one.json"))
32
+ web_minion.perform
33
+ ```
34
+
28
35
  ### Sample Flow
29
36
 
30
37
  Here's a sample login flow:
31
38
 
32
39
  {
33
- "config": {},
40
+ "config": {
41
+ driver: "mechanize",
42
+ },
34
43
  "flow": {
35
44
  "name": "Login Flow",
36
45
  "actions": [
@@ -86,6 +95,15 @@ Here's a sample login flow:
86
95
  }
87
96
  }
88
97
 
98
+
99
+
100
+ ### WebMinion Drivers
101
+ By default, WebMinion will load it's Mechanize drivers. If you want to use
102
+ Capybara, you will need to do the following:
103
+
104
+ `require "web_minion/drivers/capybara"`
105
+
106
+
89
107
  ## Development
90
108
 
91
109
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/bin/web_minion ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Usage: bin/web_minion test/test_json/capybara_flow.json
4
+ require "bundler/setup"
5
+ require "web_minion"
6
+ require "web_minion/drivers/capybara"
7
+ require "pry"
8
+ Capybara.register_driver :poltergeist do |app|
9
+ options = {
10
+ js_errors: false,
11
+ timeout: 120,
12
+ debug: false,
13
+ phantomjs_options: ["--load-images=no", "--disk-cache=false"],
14
+ inspector: false,
15
+ }
16
+ Capybara::Poltergeist::Driver.new(app, options)
17
+ end
18
+
19
+ web_minion = WebMinion::Flow.build_via_json(File.read(ARGV[0]))
20
+ result = web_minion.perform
21
+ puts result[:history].status
22
+ puts result[:saved_vars].to_json if result[:saved_vars]
data/lib/web_minion.rb CHANGED
@@ -1,6 +1,9 @@
1
- require "web_minion/bots/bot"
2
- require "web_minion/bots/mechanize_bot"
1
+ require "web_minion/drivers/mechanize"
3
2
  require "web_minion/step"
4
3
  require "web_minion/action"
5
4
  require "web_minion/flow"
6
5
  require "web_minion/histories/history"
6
+ module WebMinion
7
+ class MultipleOptionsFoundError < StandardError; end
8
+ class NoInputFound < StandardError; end
9
+ end
@@ -16,8 +16,16 @@ module WebMinion
16
16
  send("steps=", fields[:steps])
17
17
  end
18
18
 
19
- def self.build_from_hash(fields = {})
20
- steps = fields["steps"].map { |step| Step.new(step) }
19
+ def self.build_from_hash(fields = {}, vars = {})
20
+ steps = fields["steps"].map do |step|
21
+ begin
22
+ Step.new(step.merge("vars" => vars))
23
+ rescue NoValueForVariableError => e
24
+ (step["skippable"] && (step["is_validator"].nil? || !step["is_validator"])) ? nil : raise(e, "Current step is missing variable. (step: #{step['name']})")
25
+ end
26
+ end
27
+ steps = steps.reject(&:nil?)
28
+
21
29
  starting = (fields["starting"] || "false") == "false" ? false : true
22
30
  new(name: fields["name"], steps: steps, key: fields["key"],
23
31
  starting: starting, on_success: fields["on_success"],
@@ -50,16 +58,16 @@ module WebMinion
50
58
  end
51
59
 
52
60
  # Again, boilerplate for initial setup
53
- def perform(bot)
61
+ def perform(bot, saved_values)
54
62
  element = nil
55
63
  status = @steps.map do |step|
56
64
  if step.validator?
57
- step.perform(bot, element)
65
+ step.perform(bot, element, saved_values)
58
66
  else
59
67
  if step.retain?
60
- step.perform(bot, element)
68
+ step.perform(bot, element, saved_values)
61
69
  else
62
- element = step.perform(bot, element)
70
+ element = step.perform(bot, element, saved_values)
63
71
  end
64
72
  nil
65
73
  end
@@ -7,8 +7,12 @@ module WebMinion
7
7
  @config = config
8
8
  end
9
9
 
10
- def execute_step(method, target, value = nil, element = nil)
11
- method(method).call(target, value, element)
10
+ def execute_step(method, target, value = nil, element = nil, values_hash = {})
11
+ if method == :save_value
12
+ method(method).call(target, value, element, values_hash)
13
+ else
14
+ method(method).call(target, value, element)
15
+ end
12
16
  end
13
17
  end
14
18
  end
@@ -0,0 +1,315 @@
1
+ require "capybara"
2
+ require "web_minion/bots/bot"
3
+ require "forwardable"
4
+
5
+ module WebMinion
6
+ class CapybaraBot < WebMinion::Bot
7
+ extend Forwardable
8
+ attr_reader :bot
9
+ delegate [:body] => :@bot
10
+
11
+ # Initializes a CapybaraBot
12
+ #
13
+ # @param config [Hash] the configuration for the CapybaraBot
14
+ # @option options [Symbol] :driver The Capybara Driver to use.
15
+ # @return [CapybaraBot]
16
+ def initialize(config = {})
17
+ super(config)
18
+ @driver = config.fetch("driver").to_sym
19
+
20
+ if block_given?
21
+ yield
22
+ else
23
+ Capybara.register_driver @driver do |app|
24
+ Capybara::Selenium::Driver.new(app, browser: @driver)
25
+ end unless Capybara.drivers.include?(@driver)
26
+ end
27
+
28
+ @bot = Capybara::Session.new(@driver)
29
+ @bot.driver.resize(config["dimensions"]["width"], config["dimensions"]["height"]) if config["dimensions"]
30
+ end
31
+
32
+ def page
33
+ @bot.html
34
+ end
35
+
36
+ # Goes to the provided url
37
+ #
38
+ # @param target [String] the target (URL) of the site to visit.
39
+ # @param _value [String] the value (unused)
40
+ # @param _element [Capybara::Node::Element] the element (unused)
41
+ # @return [nil]
42
+ def go(target, _value, _element)
43
+ @bot.visit(target)
44
+ end
45
+
46
+ # Clicks the provided target.
47
+ #
48
+ # @param target [String] the target (css or xpath) to be clicked
49
+ # @param _value [String] the value (unused)
50
+ # @param _element [Capybara::Node::Element] the element (unused)
51
+ # @return [nil]
52
+ def click(target, _value, _element)
53
+ @bot.click_link_or_button(target)
54
+ end
55
+
56
+ # Clicks the button in the provided form
57
+ #
58
+ # @param target [String] the target (css or xpath) to be clicked
59
+ # @param _value [String] the value (unused)
60
+ # @param element [Capybara::Node::Element] the element (form) containing the target
61
+ # @return [nil]
62
+ def click_button_in_form(target, _value, element)
63
+ element.find(target).click
64
+ end
65
+
66
+ # Sets the file to be uploaded
67
+ #
68
+ # @param target [String] the target
69
+ # @param value [String] the value
70
+ # @param element [Capybara::Node::Element] the element
71
+ # @return [nil]
72
+ def set_file_upload(target, value, element)
73
+ if target.is_a?(String) && %w(first last).include?(target)
74
+ file_upload = element.find_all(:css, "input[type='file']").send(target)
75
+ elsif target.is_a?(String)
76
+ target_type = %r{^//} =~ target ? :xpath : :css
77
+ file_upload = element.find(target_type, target, match: :first)
78
+ elsif target.is_a?(Hash)
79
+ key, input_name = target.first
80
+ locator = "input[#{key}='#{input_name}']"
81
+ file_upload = element.find(:css, locator, match: :first)
82
+ end
83
+
84
+ raise Errno::ENOENT unless File.exist?(File.absolute_path(value))
85
+
86
+ file_upload.set(File.absolute_path(value))
87
+ end
88
+
89
+ # Saves the current page.
90
+ #
91
+ # @param _target [String] the target (unused)
92
+ # @param value [String] the value, i.e. the filename
93
+ # @param _element [Capybara::Node::Element] the element (unused)
94
+ # @return [String]
95
+ # Examples:
96
+ #
97
+ # bot.save_html(nil, "/tmp/myfile-%{timestamp}.html")
98
+ # # => "/tmp/myfile-%{timestamp}.html"
99
+ def save_page_html(_target, value, _element)
100
+ filename = value % { timestamp: Time.now.strftime("%Y-%m-%d_%H-%M-%S") }
101
+ @bot.save_page(filename)
102
+ end
103
+
104
+ # Saves a value to a given hash
105
+ #
106
+ # @param target [String] the target
107
+ # @param value [String] the value
108
+ # @param element [Capybara::Node::Element] the element
109
+ # @return [Hash]
110
+ def save_value(target, value, _element, val_hash)
111
+ target_type = %r{^/} =~ target ? :xpath : :css
112
+ elements = @bot.find_all(target_type, target)
113
+ return val_hash if elements.empty?
114
+
115
+ val_hash[value.to_sym] = if elements.size == 1
116
+ Nokogiri::XML(elements.first["outerHTML"]).children.first
117
+ else
118
+ val_hash[value.to_sym] = elements.map { |e| Nokogiri::XML(e["outerHTML"]).children.first }
119
+ end
120
+
121
+ val_hash
122
+ end
123
+
124
+ ## FORM METHODS ##
125
+ # Must have an element passed to them (except get form)
126
+
127
+ # Gets a form element
128
+ #
129
+ # @param target [String] the target
130
+ # @param value [String] the value
131
+ # @param element [Capybara::Node::Element] the element
132
+ # @return [Capybara::Node::Element]
133
+ def get_form(target, _value, _element)
134
+ if target.is_a?(Hash)
135
+ type, target = target.first
136
+ return @bot.find(type, target)
137
+ elsif target.is_a?(String)
138
+ index = %w(first last).index(target)
139
+ return @bot.find(target) if index < 0
140
+ end
141
+
142
+ index = target if index < 0
143
+
144
+ @bot.find_all("form")[index]
145
+ end
146
+
147
+ # Finds the form field for a given element.
148
+ #
149
+ # @param target [String] the target (name) of the field
150
+ # @param _value [String] the value (unused)
151
+ # @param element [Capybara::Node::Element] the element containing the field
152
+ # @return [Capybara::Node::Element]
153
+ def get_field(target, _value, element)
154
+ # raise no element passed in? Invalid element?
155
+ element.find_field(target)
156
+ end
157
+
158
+ # Fills in a input element
159
+ #
160
+ # @param target [String] the target
161
+ # @param value [String] the value
162
+ # @param element [Capybara::Node::Element] the element
163
+ # @return [Capybara::Node::Element]
164
+ def fill_in_input(target, value, element)
165
+ key, input_name = target.first
166
+ input = element.find("input[#{key}='#{input_name}']")
167
+ raise(NoInputFound, "For target: #{target}") unless input
168
+ input.set value
169
+
170
+ element
171
+ end
172
+
173
+ # Submit a form
174
+ #
175
+ # @param _target [String] the target (unused)
176
+ # @param _value [String] the value (unused)
177
+ # @param element [Capybara::Node::Element] the element
178
+ # @return [nil]
179
+ def submit(_target, _value, element)
180
+ element.find('input[type="submit"]').click
181
+ rescue Capybara::ElementNotFound
182
+ element.click
183
+ end
184
+
185
+ # Selects the options from a <select> input.Clicks/
186
+ #
187
+ # @param target [String] the target, the label or value for the Select Box
188
+ # @param value [String] the value
189
+ # @param element [Capybara::Node::Element] the element
190
+ # @return [Capybara::Node::Element]
191
+ def select_field(target, _value, element)
192
+ # NOTE: Capybara selects from the option's label, i.e. the text, not the
193
+ # option value. If it can't find the matching text, it raises a
194
+ # Capybara::ElementNotFound error. In this situation, we should find
195
+ # that select option manually.
196
+ #
197
+ # <select>
198
+ # <option value="1">Hello</option>
199
+ # <option value="2">Hi</option>
200
+ # </select>
201
+ #
202
+ # These two commands are equivalent
203
+ #
204
+ # 1. element.select("Hello")
205
+ # 2. element.find("option[value='1']").select_option
206
+ if target.is_a?(Hash)
207
+ key, value = target.first
208
+ element.find("option[#{key}='#{value}']").select_option
209
+ else
210
+ element.select(target)
211
+ end
212
+ rescue Capybara::ElementNotFound
213
+ element.find("option[value='#{target}']").select_option
214
+ rescue Capybara::Ambiguous
215
+ raise(MultipleOptionsFoundError, "For target: #{target}")
216
+ end
217
+
218
+ # Checks a checkbox
219
+ #
220
+ # @param target [String] the target
221
+ # @param value [String] the value
222
+ # @param element [Capybara::Node::Element] the element
223
+ # @return [nil]
224
+ def select_checkbox(target, _value, element)
225
+ if target.is_a?(Array)
226
+ target.each do |tar|
227
+ key, value = tar.first
228
+ element.find(:css, "input[#{key}='#{value}']").set(true)
229
+ end
230
+ else
231
+ begin
232
+ element.check(target)
233
+ rescue Capybara::ElementNotFound
234
+ element.find(:css, target).set(true)
235
+ end
236
+ end
237
+ end
238
+
239
+ # Select a radio button
240
+ #
241
+ # @param target [String] the target
242
+ # @param value [String] the value
243
+ # @param element [Capybara::Node::Element] the element
244
+ # @return [Capybara::Node::Element]
245
+ def select_radio_button(target, value, element)
246
+ if target.is_a?(Array)
247
+ return target.map { |tar| select_radio_button(tar, value, element) }
248
+ elsif target.is_a?(Hash)
249
+ key, value = target.first
250
+ radio = element.find(:css, "input[#{key}='#{value}']")
251
+ radio.set(true)
252
+ else
253
+ begin
254
+ element.choose(target)
255
+ rescue Capybara::ElementNotFound
256
+ radio = element.find(:css, target)
257
+ radio.set(true)
258
+ end
259
+ end
260
+
261
+ radio || element.find(target)
262
+ end
263
+
264
+ # Selects the first radio button
265
+ #
266
+ # @param target [String] the target
267
+ # @param value [String] the value
268
+ # @param element [Capybara::Node::Element] the element
269
+ # @return [Capybara::Node::Element]
270
+ def select_first_radio_button(_target, _value, element)
271
+ radio = element.find(:css, "input[type='radio']", match: :first)
272
+ radio.set(true)
273
+
274
+ radio
275
+ end
276
+
277
+ ## VALIDATION METHODS ##
278
+
279
+ # Tests if the value provided equals the current URL.
280
+ #
281
+ # @param _target [String] the target (unused)
282
+ # @param value [String] the value
283
+ # @param _element [Capybara::Node::Element] the element (unused)
284
+ # @return [Boolean]
285
+ def url_equals(_target, value, _element)
286
+ !!(@bot.current_url == value)
287
+ end
288
+
289
+ # Tests if the body includes the value or values provided.
290
+ #
291
+ # @param target [String] the target
292
+ # @param value [String, Regexp, Array[String, Regexp]] the value
293
+ # @param element [Capybara::Node::Element] the element
294
+ # @return [Boolean]
295
+ def body_includes(target, value, element)
296
+ if value.is_a?(Array)
297
+ # FIXME: this should probably return true if all the values exist.
298
+ val_check_arr = value.map { |v| body_includes(target, v, element) }
299
+ val_check_arr.uniq.include?(true)
300
+ else
301
+ !!(body.index(value) && body.index(value) > 0)
302
+ end
303
+ end
304
+
305
+ # Tests if the value provided equals the value of the element
306
+ #
307
+ # @param _target [String] the target (unused)
308
+ # @param value [String] the value
309
+ # @param element [Capybara::Node::Element] the element
310
+ # @return [Boolean]
311
+ def value_equals(_target, value, element)
312
+ !!(element && (element.value == value))
313
+ end
314
+ end
315
+ end
@@ -0,0 +1,40 @@
1
+ require 'web_minion/bots/elements/file_upload_element'
2
+
3
+ class FileUploadElement < MechanizeElement
4
+ def initialize(bot, target, value, element)
5
+ super(bot, target, value, element)
6
+ end
7
+
8
+ def set_file
9
+ case @target_type
10
+ when :index
11
+ index_set
12
+ when :string_path
13
+ string_set
14
+ when :first_last
15
+ first_last_set
16
+ else
17
+ raise(InvalidTargetType, "#{@target_type} is not valid!")
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def index_set
24
+ @element.file_uploads[@target].file_name = @value
25
+ end
26
+
27
+ def string_set
28
+ @element.file_upload_with(@target).file_name = @value
29
+ end
30
+
31
+ def first_last_set
32
+ if @target == "first"
33
+ @element.file_uploads.first.file_name = @value
34
+ elsif @target == "last"
35
+ @element.file_uploads.last.file_name = @value
36
+ else
37
+ raise(InvalidTargetType, "#{@target} is not first or last!")
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,40 @@
1
+ require "web_minion/bots/elements/mechanize_element"
2
+
3
+ class FormElement < MechanizeElement
4
+ def initialize(bot, target, value = nil, element = nil)
5
+ super(bot, target, value, element)
6
+ end
7
+
8
+ def get
9
+ case @target_type
10
+ when :index
11
+ index_get
12
+ when :string_path
13
+ string_get
14
+ when :first_last
15
+ first_last_get
16
+ else
17
+ raise(InvalidTargetType, "#{@target_type} is not valid!")
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def first_last_get
24
+ if @target == "first"
25
+ @bot.page.forms.first
26
+ elsif @target == "last"
27
+ @bot.page.forms.last
28
+ else
29
+ raise(InvalidTargetType, "#{@target} is not first or last!")
30
+ end
31
+ end
32
+
33
+ def index_get
34
+ @bot.page.forms[@target]
35
+ end
36
+
37
+ def string_get
38
+ @bot.page.form_with(@target)
39
+ end
40
+ end
@@ -0,0 +1,27 @@
1
+ class MechanizeElement
2
+ class InvalidTargetType < StandardError; end
3
+
4
+ attr_reader :bot, :target, :value, :element, :target_type
5
+
6
+ def initialize(bot, target, value = nil, element = nil)
7
+ @bot = bot
8
+ @target = target
9
+ @target_type = determine_target_type(target)
10
+ @value = value
11
+ @element = element
12
+ end
13
+
14
+ private
15
+
16
+ def determine_target_type(target)
17
+ if target.is_a? Integer
18
+ return :index
19
+ else
20
+ if %w(first last).include?(target)
21
+ return :first_last
22
+ end
23
+
24
+ :string_path
25
+ end
26
+ end
27
+ end
@@ -1,9 +1,9 @@
1
1
  require "mechanize"
2
2
  require "web_minion/bots/bot"
3
+ require "web_minion/bots/elements/form_element"
4
+ require "web_minion/bots/elements/file_upload_element"
3
5
 
4
- class MultipleOptionsFoundError < StandardError; end
5
- class NoInputFound < StandardError; end
6
- # Mechanize driven bot. More efficient, but can"t handle any dynamic js-driven
6
+ # Mechanize driven bot. More efficient, but can't handle any dynamic js-driven
7
7
  # pages
8
8
  module WebMinion
9
9
  class MechanizeBot < WebMinion::Bot
@@ -33,19 +33,39 @@ module WebMinion
33
33
  element.button_with(target).click
34
34
  end
35
35
 
36
+ def set_file_upload(target, value, element)
37
+ FileUploadElement.new(@bot, target, value, element).set_file
38
+ end
39
+
40
+ # ability to save dynamic dates in the filename with string "INSERT_DATE"
41
+ # so a value of "myfilename-INSERT_DATE.html" will look like this:
42
+ # "myfilename-2016-09-19_18-51-40.html"
43
+ # if you want multiple steps saving at the same time without overwriting,
44
+ # rename save_html step in your json (i.e. 'save_html_confirmation' etc)
36
45
  def save_page_html(_target, value, _element)
46
+ if value.include?("INSERT_DATE")
47
+ time = Time.now.strftime('%Y-%m-%d_%H-%M-%S').to_s
48
+ value = value.split("INSERT_DATE").insert(1, time).join
49
+ end
37
50
  write_html_file(value)
38
51
  end
39
52
 
40
- ## FORM METHODS ##
41
- # Must have an element passed to them (except get form)
53
+ def save_value(target, value, _element, val_hash)
54
+ element = @bot.page.search(target)
42
55
 
43
- def get_form_in_index(target, _value, _element)
44
- @bot.page.forms[target]
56
+ if val_hash[value.to_sym]
57
+ val_hash[value.to_sym] << element if element
58
+ else
59
+ val_hash[value.to_sym] = element if element
60
+ end
61
+
62
+ val_hash
45
63
  end
46
64
 
65
+ ## FORM METHODS ##
66
+ # Must have an element passed to them (except get form)
47
67
  def get_form(target, _value, _element)
48
- @bot.page.form_with(target)
68
+ FormElement.new(@bot, target, nil, nil).get
49
69
  end
50
70
 
51
71
  def get_field(target, _value, element)
@@ -71,7 +91,11 @@ module WebMinion
71
91
  end
72
92
 
73
93
  def select_checkbox(target, _value, element)
74
- element.checkbox_with(target).check
94
+ if target.is_a?(Array)
95
+ target.each { |tar| select_checkbox(tar, nil, element) }
96
+ else
97
+ element.checkbox_with(target).check
98
+ end
75
99
  end
76
100
 
77
101
  def select_radio_button(target, _value, element)
@@ -93,7 +117,15 @@ module WebMinion
93
117
  end
94
118
 
95
119
  def body_includes(_target, value, _element)
96
- !!(body.index(value) && body.index(value) > 0)
120
+ if value.is_a?(Array)
121
+ val_check_arr = []
122
+ value.each do |val|
123
+ val_check_arr << !!(body.index(val) && body.index(val) > 0)
124
+ end
125
+ val_check_arr.uniq.include?(true)
126
+ else
127
+ !!(body.index(value) && body.index(value) > 0)
128
+ end
97
129
  end
98
130
 
99
131
  def value_equals(_target, value, element)
@@ -0,0 +1,2 @@
1
+ require "capybara/poltergeist" if $LOAD_PATH.select { |p| p.include? "poltergeist" }
2
+ require "web_minion/bots/capybara_bot"
@@ -0,0 +1 @@
1
+ require "web_minion/bots/mechanize_bot"
@@ -13,25 +13,32 @@ module WebMinion
13
13
  class NoStartingActionError < StandardError; end
14
14
  class CyclicalFlowError < StandardError; end
15
15
 
16
- attr_accessor :actions, :bot, :history
17
- attr_writer :name
18
- attr_reader :curr_action, :starting_action
16
+ attr_accessor :actions, :bot, :history, :name, :vars
17
+ attr_reader :curr_action, :starting_action, :saved_values
19
18
 
20
- def initialize(actions, bot, name = "")
19
+ def initialize(actions, bot, vars = {}, name = "")
21
20
  @actions = actions
22
21
  @bot = bot
23
22
  @name = name
23
+ @vars = vars
24
24
  @history = nil
25
+ @saved_values = {}
25
26
  end
26
27
 
27
- def self.build_via_json(rule_json)
28
+ def self.build_via_json(rule_json, vars = {})
28
29
  ruleset = JSON.parse(rule_json)
29
- bot = MechanizeBot.new(ruleset["config"])
30
- build_from_hash(ruleset["flow"].merge(bot: bot))
30
+ driver = ruleset["config"]["driver"] || "mechanize"
31
+ bot = if driver == "mechanize"
32
+ MechanizeBot.new(ruleset["config"])
33
+ else
34
+ CapybaraBot.new(ruleset["config"])
35
+ end
36
+ build_from_hash(ruleset["flow"].merge(bot: bot, vars: vars))
31
37
  end
32
38
 
33
39
  def self.build_from_hash(fields = {})
34
40
  flow = new([], nil, nil)
41
+ flow.vars = fields[:vars] if fields[:vars]
35
42
  fields.each_pair do |k, v|
36
43
  flow.send("#{k}=", v)
37
44
  end
@@ -41,7 +48,7 @@ module WebMinion
41
48
  def actions=(actions)
42
49
  @actions = {}
43
50
  actions.each do |act|
44
- action = Action.build_from_hash(act)
51
+ action = Action.build_from_hash(act, @vars)
45
52
  @actions[action.key] = action
46
53
  @starting_action = action if action.starting_action?
47
54
  end
@@ -56,23 +63,30 @@ module WebMinion
56
63
 
57
64
  def perform
58
65
  @history = FlowHistory.new
59
- status = execute_action(@starting_action)
66
+ status = execute_action(@starting_action, @saved_values)
60
67
  @history.end_time = Time.now
61
68
  @history.status = status
62
- @history
69
+ results
63
70
  end
64
71
 
65
72
  private
66
73
 
67
- def execute_action(action)
74
+ def results
75
+ {
76
+ history: @history,
77
+ saved_values: @saved_values
78
+ }
79
+ end
80
+
81
+ def execute_action(action, saved_values = {})
68
82
  @curr_action = action
69
83
  @history.action_history << ActionHistory.new(action.name, action.key)
70
- status = action.perform(@bot)
84
+ status = action.perform(@bot, saved_values)
71
85
  update_action_history(status)
72
86
  if status
73
- action.ending_action? ? true : execute_action(action.on_success)
87
+ action.ending_action? ? true : execute_action(action.on_success, saved_values)
74
88
  else
75
- action.on_failure ? execute_action(action.on_failure) : false
89
+ action.on_failure ? execute_action(action.on_failure, saved_values) : false
76
90
  end
77
91
  end
78
92
 
@@ -2,6 +2,10 @@ module WebMinion
2
2
  # Histories are used to log the events as the bot performs its flows, steps,
3
3
  # and actions
4
4
  class History
5
+ class InvalidStatus < StandardError; end
6
+
7
+ VALID_STATUSES = %w(Successful Unsuccessful Skipped).freeze
8
+
5
9
  attr_reader :runtime, :status, :start_time, :end_time
6
10
 
7
11
  def initialize(start_time = nil)
@@ -10,12 +14,27 @@ module WebMinion
10
14
  end
11
15
 
12
16
  def status=(status)
13
- @status = status ? "Successful" : "Unsuccessful"
17
+ @status = parse_status(status)
14
18
  end
15
19
 
16
20
  def end_time=(end_time)
17
21
  @end_time = end_time
18
22
  @runtime = @end_time - @start_time if @start_time && @end_time
19
23
  end
24
+
25
+ private
26
+
27
+ def parse_status(status)
28
+ if [TrueClass, FalseClass].include? status.class
29
+ status ? "Successful" : "Unsuccessful"
30
+ elsif [String, Symbol].include? status.class
31
+ unless VALID_STATUSES.include? status.capitalize
32
+ raise(InvalidStatus, "#{status} is not a valid!")
33
+ end
34
+ status.to_s.capitalize
35
+ else
36
+ raise(InvalidStatus, "#{status} must be a boolean, string, or symbol.")
37
+ end
38
+ end
20
39
  end
21
40
  end
@@ -1,45 +1,63 @@
1
1
  module WebMinion
2
2
  class InvalidMethodError < StandardError; end
3
-
3
+ class NoValueForVariableError < StandardError; end
4
4
  # A Step represents the individual operation that the bot will perform. This
5
5
  # often includes grabbing an element from the DOM tree, or performing some
6
6
  # operation on an element that has already been found.
7
7
  class Step
8
- attr_accessor :name, :target, :method, :value, :is_validator, :retain_element
9
-
10
- VALID_METHODS = [
11
- :go,
12
- :select,
13
- :click,
14
- :click_button_in_form,
15
- :get_form,
16
- :get_form_in_index,
17
- :get_field,
18
- :select_field,
19
- :select_radio_button,
20
- :select_first_radio_button,
21
- :select_checkbox,
22
- :submit,
23
- :fill_in_input,
24
- :url_equals,
25
- :value_equals,
26
- :body_includes,
27
- :save_page_html
28
- ].freeze
8
+ attr_accessor :name, :target, :method, :value, :is_validator, :retain_element, :skippable
9
+ attr_reader :saved_values, :vars
10
+
11
+ VALID_METHODS = {
12
+ select: [
13
+ :field,
14
+ :radio_button,
15
+ :first_radio_button,
16
+ :checkbox
17
+ ],
18
+ main_methods: [
19
+ :set_file_upload,
20
+ :get_field,
21
+ :get_form,
22
+ :go,
23
+ :select,
24
+ :click,
25
+ :click_button_in_form,
26
+ :submit,
27
+ :fill_in_input,
28
+ :url_equals,
29
+ :value_equals,
30
+ :body_includes,
31
+ :save_page_html,
32
+ :save_value
33
+ ]
34
+ }.freeze
29
35
 
30
36
  def initialize(fields = {})
31
37
  fields.each_pair do |k, v|
32
- send("#{k}=", v)
38
+ if valid_method?(k.to_sym)
39
+ send("method=", k)
40
+ @target = v
41
+ else
42
+ send("#{k}=", v)
43
+ end
33
44
  end
45
+
46
+ replace_all_variables
47
+ end
48
+
49
+ def vars=(vars)
50
+ @vars = Hash[vars.collect{ |k, v| [k.to_s, v] }]
34
51
  end
35
52
 
36
- def perform(bot, element = nil)
37
- bot.execute_step(@method, @target, @value, element)
53
+ def perform(bot, element = nil, saved_values)
54
+ bot.execute_step(@method, @target, @value, element, saved_values)
38
55
  end
39
56
 
40
57
  def method=(method)
41
58
  raise(InvalidMethodError, "Method: #{method} is not valid") unless valid_method?(method.to_sym)
42
- @method = method.to_sym
59
+ split = method.to_s.split("/").map(&:to_sym)
60
+ @method = split.count > 1 ? "#{split[0]}_#{split[1]}".to_sym : method.to_sym
43
61
  end
44
62
 
45
63
  def retain?
@@ -51,7 +69,42 @@ module WebMinion
51
69
  end
52
70
 
53
71
  def valid_method?(method)
54
- VALID_METHODS.include?(method)
72
+ split = method.to_s.split("/").map(&:to_sym)
73
+ if split.count > 1
74
+ return true if VALID_METHODS[split[0]].include?(split[1])
75
+ end
76
+ VALID_METHODS[:main_methods].include?(method)
77
+ end
78
+
79
+ private
80
+
81
+ def replace_all_variables
82
+ %w(value target).each do |field|
83
+ next if send(field).nil?
84
+ send("#{field}=", replace_variable(send(field)))
85
+ end
86
+ end
87
+
88
+ def replace_variable(var)
89
+ if var.is_a?(Hash)
90
+ return handle_hash_replacement(var)
91
+ else
92
+ return var unless var.is_a?(String)
93
+ # This will handle email addresses
94
+ return var if var.match(/\w+@\D+\.\D+/)
95
+ if replace_var = var.match(/@(\D+)/)
96
+ raise(NoValueForVariableError, "no variable to use found for #{replace_var}") unless @vars[replace_var[1]]
97
+ var = @vars[replace_var[1]]
98
+ end
99
+ end
100
+ var
101
+ end
102
+
103
+ def handle_hash_replacement(hash)
104
+ hash.each_pair do |k, v|
105
+ hash[k] = replace_variable(v)
106
+ end
107
+ hash
55
108
  end
56
109
  end
57
110
  end
@@ -1,3 +1,3 @@
1
1
  module WebMinion
2
- VERSION = "0.1.0".freeze
2
+ VERSION = "0.3.0".freeze
3
3
  end
data/web_minion.gemspec CHANGED
@@ -16,7 +16,7 @@ Gem::Specification.new do |spec|
16
16
 
17
17
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
18
  spec.bindir = "bin"
19
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.executables = "web_minion"
20
20
  spec.require_paths = ["lib"]
21
21
 
22
22
  spec.add_development_dependency "bundler"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: web_minion
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Parrish
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-06-21 00:00:00.000000000 Z
11
+ date: 2016-11-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -56,8 +56,7 @@ description: A metadata-driven browser automation.
56
56
  email:
57
57
  - m.andrewparrish@gmail.com
58
58
  executables:
59
- - console
60
- - setup
59
+ - web_minion
61
60
  extensions: []
62
61
  extra_rdoc_files: []
63
62
  files:
@@ -71,12 +70,18 @@ files:
71
70
  - Rakefile
72
71
  - bin/console
73
72
  - bin/setup
74
- - lib/jibe_ruleset_bot/version.rb
73
+ - bin/web_minion
75
74
  - lib/web_minion.rb
76
75
  - lib/web_minion/action.rb
77
76
  - lib/web_minion/bots/bot.rb
77
+ - lib/web_minion/bots/capybara_bot.rb
78
+ - lib/web_minion/bots/elements/file_upload_element.rb
79
+ - lib/web_minion/bots/elements/form_element.rb
80
+ - lib/web_minion/bots/elements/mechanize_element.rb
78
81
  - lib/web_minion/bots/mechanize_bot.rb
79
82
  - lib/web_minion/cycle_checker.rb
83
+ - lib/web_minion/drivers/capybara.rb
84
+ - lib/web_minion/drivers/mechanize.rb
80
85
  - lib/web_minion/flow.rb
81
86
  - lib/web_minion/histories/action_history.rb
82
87
  - lib/web_minion/histories/flow_history.rb
@@ -1,3 +0,0 @@
1
- module WebMinion
2
- VERSION = "0.1.0".freeze
3
- end