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 +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
|
- [](https://travis-ci.org/jibeinc/web_minion)
|
3
3
|
- [](https://codeclimate.com/github/jibeinc/web_minion)
|
4
|
-
- [](https://
|
4
|
+
- [](https://coveralls.io/github/jibeinc/web_minion)
|
5
5
|
- [](http://github.com/jibeinc/web_minion/issues)
|
6
6
|
- [](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
|