web_minion 0.1.0 → 0.3.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 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