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 +4 -4
- data/.gitignore +6 -0
- data/.travis.yml +3 -3
- data/Gemfile +3 -0
- data/README.md +21 -3
- data/bin/web_minion +22 -0
- data/lib/web_minion.rb +5 -2
- data/lib/web_minion/action.rb +14 -6
- data/lib/web_minion/bots/bot.rb +6 -2
- data/lib/web_minion/bots/capybara_bot.rb +315 -0
- data/lib/web_minion/bots/elements/file_upload_element.rb +40 -0
- data/lib/web_minion/bots/elements/form_element.rb +40 -0
- data/lib/web_minion/bots/elements/mechanize_element.rb +27 -0
- data/lib/web_minion/bots/mechanize_bot.rb +42 -10
- data/lib/web_minion/drivers/capybara.rb +2 -0
- data/lib/web_minion/drivers/mechanize.rb +1 -0
- data/lib/web_minion/flow.rb +28 -14
- data/lib/web_minion/histories/history.rb +20 -1
- data/lib/web_minion/step.rb +80 -27
- data/lib/web_minion/version.rb +1 -1
- data/web_minion.gemspec +1 -1
- metadata +10 -5
- data/lib/jibe_ruleset_bot/version.rb +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 477c3b18a2d94887c6d2e3ad2fa1c59b6d4dfd74
|
4
|
+
data.tar.gz: eb231e8060d78d0e59ce36c97cabba543af1a4f1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a74a18184389fe2ead049b7ece8629d952d8f61ffa73d40e40be1d30b8433a0205930278afed97df5edda13e472bd6cfed533b0f2151df8dc6b376c41b4880b7
|
7
|
+
data.tar.gz: 7df05983de55cc6d37ac372b520f4136ed521e20fd801ac5aec40d9809e7c71b0a2718577c7f3edd8c86e0af5359efa064bf83e6bffa716f468697741bfce821
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
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://
|
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
|
-
|
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/
|
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
|
data/lib/web_minion/action.rb
CHANGED
@@ -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
|
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
|
data/lib/web_minion/bots/bot.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
-
|
41
|
-
|
53
|
+
def save_value(target, value, _element, val_hash)
|
54
|
+
element = @bot.page.search(target)
|
42
55
|
|
43
|
-
|
44
|
-
|
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
|
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
|
-
|
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
|
-
|
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 @@
|
|
1
|
+
require "web_minion/bots/mechanize_bot"
|
data/lib/web_minion/flow.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
30
|
-
|
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
|
-
|
69
|
+
results
|
63
70
|
end
|
64
71
|
|
65
72
|
private
|
66
73
|
|
67
|
-
def
|
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
|
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
|
data/lib/web_minion/step.rb
CHANGED
@@ -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
|
-
|
11
|
-
|
12
|
-
:
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
:
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/web_minion/version.rb
CHANGED
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 =
|
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.
|
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-
|
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
|
-
-
|
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
|
-
-
|
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
|