web_minion 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bf7a988efff88355bcdc557cdf6824061e883bbd
4
+ data.tar.gz: 43361685a55294c9d4c21f13d5bd273b749fe5e7
5
+ SHA512:
6
+ metadata.gz: fdb3412034e560b84953577ecf12f11962c41f402d442df73a81016bf73beee5f133b395f0cdadc85631f93223364d8165b0e2a86737bba9dc63d8096ff0a2f1
7
+ data.tar.gz: acfee494e048cd9f65f05ddffc5b381f4a8af92bb6ba9b8163d5daf353a09710323bc20dadfb5e5edd212984d092c7c6b6a7d0b4f7883c5a04d9f1f59b34640c
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rubocop.yml ADDED
@@ -0,0 +1,243 @@
1
+ AllCops:
2
+ Exclude:
3
+ - "vendor/**/*"
4
+ - "db/schema.rb"
5
+ UseCache: false
6
+ Style/CollectionMethods:
7
+ Description: Preferred collection methods.
8
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#map-find-select-reduce-size
9
+ Enabled: true
10
+ PreferredMethods:
11
+ collect: map
12
+ collect!: map!
13
+ find: detect
14
+ find_all: select
15
+ reduce: inject
16
+ Style/DotPosition:
17
+ Description: Checks the position of the dot in multi-line method calls.
18
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#consistent-multi-line-chains
19
+ Enabled: true
20
+ EnforcedStyle: trailing
21
+ SupportedStyles:
22
+ - leading
23
+ - trailing
24
+ Style/FileName:
25
+ Description: Use snake_case for source file names.
26
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#snake-case-files
27
+ Enabled: false
28
+ Exclude: []
29
+ Style/GuardClause:
30
+ Description: Check for conditionals that can be replaced with guard clauses
31
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals
32
+ Enabled: false
33
+ MinBodyLength: 1
34
+ Style/IfUnlessModifier:
35
+ Description: Favor modifier if/unless usage when you have a single-line body.
36
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#if-as-a-modifier
37
+ Enabled: false
38
+ MaxLineLength: 80
39
+ Style/OptionHash:
40
+ Description: Don't use option hashes when you can use keyword arguments.
41
+ Enabled: false
42
+ Style/PercentLiteralDelimiters:
43
+ Description: Use `%`-literal delimiters consistently
44
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#percent-literal-braces
45
+ Enabled: false
46
+ PreferredDelimiters:
47
+ "%": "()"
48
+ "%i": "()"
49
+ "%q": "()"
50
+ "%Q": "()"
51
+ "%r": "{}"
52
+ "%s": "()"
53
+ "%w": "()"
54
+ "%W": "()"
55
+ "%x": "()"
56
+ Style/PredicateName:
57
+ Description: Check the names of predicate methods.
58
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#bool-methods-qmark
59
+ Enabled: true
60
+ NamePrefix:
61
+ - is_
62
+ - has_
63
+ - have_
64
+ NamePrefixBlacklist:
65
+ - is_
66
+ Exclude:
67
+ - spec/**/*
68
+ Style/RaiseArgs:
69
+ Description: Checks the arguments passed to raise/fail.
70
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#exception-class-messages
71
+ Enabled: false
72
+ EnforcedStyle: exploded
73
+ SupportedStyles:
74
+ - compact
75
+ - exploded
76
+ Style/SignalException:
77
+ Description: Checks for proper usage of fail and raise.
78
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#fail-method
79
+ Enabled: false
80
+ EnforcedStyle: semantic
81
+ SupportedStyles:
82
+ - only_raise
83
+ - only_fail
84
+ - semantic
85
+ Style/SingleLineBlockParams:
86
+ Description: Enforces the names of some block params.
87
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#reduce-blocks
88
+ Enabled: false
89
+ Methods:
90
+ - reduce:
91
+ - a
92
+ - e
93
+ - inject:
94
+ - a
95
+ - e
96
+ Style/SingleLineMethods:
97
+ Description: Avoid single-line methods.
98
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-single-line-methods
99
+ Enabled: false
100
+ AllowIfMethodIsEmpty: true
101
+ Style/StringLiterals:
102
+ Description: Checks if uses of quotes match the configured preference.
103
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#consistent-string-literals
104
+ Enabled: true
105
+ EnforcedStyle: double_quotes
106
+ SupportedStyles:
107
+ - single_quotes
108
+ - double_quotes
109
+ Style/StringLiteralsInInterpolation:
110
+ Description: Checks if uses of quotes inside expressions in interpolated strings
111
+ match the configured preference.
112
+ Enabled: true
113
+ EnforcedStyle: single_quotes
114
+ SupportedStyles:
115
+ - single_quotes
116
+ - double_quotes
117
+ Style/TrailingCommaInArguments:
118
+ Description: 'Checks for trailing comma in argument lists.'
119
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas'
120
+ Enabled: false
121
+ EnforcedStyleForMultiline: no_comma
122
+ SupportedStyles:
123
+ - comma
124
+ - consistent_comma
125
+ - no_comma
126
+ Style/TrailingCommaInLiteral:
127
+ Description: 'Checks for trailing comma in array and hash literals.'
128
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas'
129
+ Enabled: false
130
+ EnforcedStyleForMultiline: no_comma
131
+ SupportedStyles:
132
+ - comma
133
+ - consistent_comma
134
+ - no_comma
135
+ Metrics/AbcSize:
136
+ Description: A calculated magnitude based on number of assignments, branches, and
137
+ conditions.
138
+ Enabled: false
139
+ Max: 15
140
+ Metrics/ClassLength:
141
+ Description: Avoid classes longer than 100 lines of code.
142
+ Enabled: false
143
+ CountComments: false
144
+ Max: 100
145
+ Metrics/ModuleLength:
146
+ CountComments: false
147
+ Max: 100
148
+ Description: Avoid modules longer than 100 lines of code.
149
+ Enabled: false
150
+ Metrics/CyclomaticComplexity:
151
+ Description: A complexity metric that is strongly correlated to the number of test
152
+ cases needed to validate a method.
153
+ Enabled: false
154
+ Max: 6
155
+ Metrics/MethodLength:
156
+ Description: Avoid methods longer than 10 lines of code.
157
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#short-methods
158
+ Enabled: false
159
+ CountComments: false
160
+ Max: 10
161
+ Metrics/ParameterLists:
162
+ Description: Avoid parameter lists longer than three or four parameters.
163
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#too-many-params
164
+ Enabled: false
165
+ Max: 5
166
+ CountKeywordArgs: true
167
+ Metrics/PerceivedComplexity:
168
+ Description: A complexity metric geared towards measuring complexity for a human
169
+ reader.
170
+ Enabled: false
171
+ Max: 7
172
+ Lint/AssignmentInCondition:
173
+ Description: Don't use assignment in conditions.
174
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#safe-assignment-in-condition
175
+ Enabled: false
176
+ AllowSafeAssignment: true
177
+ Style/InlineComment:
178
+ Description: Avoid inline comments.
179
+ Enabled: false
180
+ Style/AccessorMethodName:
181
+ Description: Check the naming of accessor methods for get_/set_.
182
+ Enabled: false
183
+ Style/Alias:
184
+ Description: Use alias_method instead of alias.
185
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#alias-method
186
+ Enabled: false
187
+ Style/Documentation:
188
+ Description: Document classes and non-namespace modules.
189
+ Enabled: false
190
+ Style/DoubleNegation:
191
+ Description: Checks for uses of double negation (!!).
192
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-bang-bang
193
+ Enabled: false
194
+ Style/EachWithObject:
195
+ Description: Prefer `each_with_object` over `inject` or `reduce`.
196
+ Enabled: false
197
+ Style/EmptyLiteral:
198
+ Description: Prefer literals to Array.new/Hash.new/String.new.
199
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#literal-array-hash
200
+ Enabled: false
201
+ Style/ModuleFunction:
202
+ Description: Checks for usage of `extend self` in modules.
203
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#module-function
204
+ Enabled: false
205
+ Style/OneLineConditional:
206
+ Description: Favor the ternary operator(?:) over if/then/else/end constructs.
207
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#ternary-operator
208
+ Enabled: false
209
+ Style/PerlBackrefs:
210
+ Description: Avoid Perl-style regex back references.
211
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers
212
+ Enabled: false
213
+ Style/Send:
214
+ Description: Prefer `Object#__send__` or `Object#public_send` to `send`, as `send`
215
+ may overlap with existing methods.
216
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#prefer-public-send
217
+ Enabled: false
218
+ Style/SpecialGlobalVars:
219
+ Description: Avoid Perl-style global variables.
220
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-cryptic-perlisms
221
+ Enabled: false
222
+ Style/VariableInterpolation:
223
+ Description: Don't interpolate global, instance and class variables directly in
224
+ strings.
225
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#curlies-interpolate
226
+ Enabled: false
227
+ Style/WhenThen:
228
+ Description: Use when x then ... for one-line cases.
229
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#one-line-cases
230
+ Enabled: false
231
+ Lint/EachWithObjectArgument:
232
+ Description: Check for immutable argument given to each_with_object.
233
+ Enabled: true
234
+ Lint/HandleExceptions:
235
+ Description: Don't suppress exception.
236
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions
237
+ Enabled: false
238
+ Lint/LiteralInCondition:
239
+ Description: Checks of literals used in conditions.
240
+ Enabled: false
241
+ Lint/LiteralInInterpolation:
242
+ Description: Checks for literals used in interpolation.
243
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.3.1
4
+ - 2.2.5
5
+ - 2.1.9
6
+ - 2.0.0
7
+ - 1.9.3-p551
8
+ - ruby-head
@@ -0,0 +1,49 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, and in the interest of
4
+ fostering an open and welcoming community, we pledge to respect all people who
5
+ contribute through reporting issues, posting feature requests, updating
6
+ documentation, submitting pull requests or patches, and other activities.
7
+
8
+ We are committed to making participation in this project a harassment-free
9
+ experience for everyone, regardless of level of experience, gender, gender
10
+ identity and expression, sexual orientation, disability, personal appearance,
11
+ body size, race, ethnicity, age, religion, or nationality.
12
+
13
+ Examples of unacceptable behavior by participants include:
14
+
15
+ * The use of sexualized language or imagery
16
+ * Personal attacks
17
+ * Trolling or insulting/derogatory comments
18
+ * Public or private harassment
19
+ * Publishing other's private information, such as physical or electronic
20
+ addresses, without explicit permission
21
+ * Other unethical or unprofessional conduct
22
+
23
+ Project maintainers have the right and responsibility to remove, edit, or
24
+ reject comments, commits, code, wiki edits, issues, and other contributions
25
+ that are not aligned to this Code of Conduct, or to ban temporarily or
26
+ permanently any contributor for other behaviors that they deem inappropriate,
27
+ threatening, offensive, or harmful.
28
+
29
+ By adopting this Code of Conduct, project maintainers commit themselves to
30
+ fairly and consistently applying these principles to every aspect of managing
31
+ this project. Project maintainers who do not follow or enforce the Code of
32
+ Conduct may be permanently removed from the project team.
33
+
34
+ This code of conduct applies both within project spaces and in public spaces
35
+ when an individual is representing the project or its community.
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
38
+ reported by contacting a project maintainer at aparrish@jibe.com. All
39
+ complaints will be reviewed and investigated and will result in a response that
40
+ is deemed necessary and appropriate to the circumstances. Maintainers are
41
+ obligated to maintain confidentiality with regard to the reporter of an
42
+ incident.
43
+
44
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
45
+ version 1.3.0, available at
46
+ [http://contributor-covenant.org/version/1/3/0/][version]
47
+
48
+ [homepage]: http://contributor-covenant.org
49
+ [version]: http://contributor-covenant.org/version/1/3/0/
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "mechanize"
4
+
5
+ group :development do
6
+ gem "pry"
7
+ end
8
+
9
+ group :test do
10
+ gem "minitest"
11
+ gem "minitest-reporters"
12
+ gem "coveralls", require: false
13
+ end
14
+
15
+ # Specify your gem"s dependencies in web_minion.gemspec
16
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Andrew Parrish
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # WebMinion
2
+ - [![Build](http://img.shields.io/travis-ci/jibeinc/web_minion.svg?style=flat-square)](https://travis-ci.org/jibeinc/web_minion)
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)
5
+ - [![Issues](http://img.shields.io/github/issues/jibeinc/web_minion.svg?style=flat-square)](http://github.com/jibeinc/web_minion/issues)
6
+ - [![License](http://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](http://opensource.org/licenses/MIT)
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.
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ ```ruby
15
+ gem 'web_minion'
16
+ ```
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Or install it yourself as:
23
+
24
+ $ gem install web_minion
25
+
26
+ ## Usage
27
+
28
+ ### Sample Flow
29
+
30
+ Here's a sample login flow:
31
+
32
+ {
33
+ "config": {},
34
+ "flow": {
35
+ "name": "Login Flow",
36
+ "actions": [
37
+ {
38
+ "name": "Login",
39
+ "key": "load_login_page",
40
+ "starting": true,
41
+ "steps":
42
+ [
43
+ {
44
+ "name": "Go to accounts login page",
45
+ "target": "https://example.com/login",
46
+ "method": "go"
47
+ },
48
+ {
49
+ "name": "Get login form",
50
+ "method": "get_form",
51
+ "target": {
52
+ "name": "login"
53
+ }
54
+ },
55
+ {
56
+ "name": "Fill in email",
57
+ "method": "fill_in_input",
58
+ "retain_element": true,
59
+ "target": {
60
+ "id": "email"
61
+ },
62
+ "value": "PUT USERNAME HERE"
63
+ },
64
+ {
65
+ "name": "Fill in password",
66
+ "method": "fill_in_input",
67
+ "retain_element": true,
68
+ "target": {
69
+ "id": "password"
70
+ },
71
+ "value": "PUT PASSWORD HERE"
72
+ },
73
+ {
74
+ "name": "Submit login",
75
+ "method": "submit"
76
+ },
77
+ {
78
+ "name": "Verify logged in page",
79
+ "is_validator": true,
80
+ "method": "body_includes",
81
+ "value": "Welcome Back"
82
+ }
83
+ ]
84
+ }
85
+ ]
86
+ }
87
+ }
88
+
89
+ ## Development
90
+
91
+ 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.
92
+
93
+ To install this gem onto your local machine, run `bundle exec rake install`.
94
+
95
+ ## Contributing
96
+
97
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jibeinc/web_minion. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
98
+
99
+
100
+ ## License
101
+
102
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "web_minion"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,3 @@
1
+ module WebMinion
2
+ VERSION = "0.1.0".freeze
3
+ end
data/lib/web_minion.rb ADDED
@@ -0,0 +1,6 @@
1
+ require "web_minion/bots/bot"
2
+ require "web_minion/bots/mechanize_bot"
3
+ require "web_minion/step"
4
+ require "web_minion/action"
5
+ require "web_minion/flow"
6
+ require "web_minion/histories/history"
@@ -0,0 +1,73 @@
1
+ require "web_minion/step"
2
+
3
+ module WebMinion
4
+ # Represents a group of steps that the bot can perform and valdiate have
5
+ # performed as expected
6
+ class Action
7
+ attr_reader :name, :key, :steps, :starting_action
8
+ attr_accessor :on_success, :on_failure
9
+
10
+ def initialize(fields = {})
11
+ @name = fields[:name]
12
+ @key = fields[:key] || @name
13
+ @starting_action = fields[:starting]
14
+ @on_success = fields[:on_success]
15
+ @on_failure = fields[:on_failure]
16
+ send("steps=", fields[:steps])
17
+ end
18
+
19
+ def self.build_from_hash(fields = {})
20
+ steps = fields["steps"].map { |step| Step.new(step) }
21
+ starting = (fields["starting"] || "false") == "false" ? false : true
22
+ new(name: fields["name"], steps: steps, key: fields["key"],
23
+ starting: starting, on_success: fields["on_success"],
24
+ on_failure: fields["on_failure"])
25
+ end
26
+
27
+ def starting_action?
28
+ @starting_action
29
+ end
30
+
31
+ def ending_action?
32
+ @on_success.nil?
33
+ end
34
+
35
+ def next_actions
36
+ [on_success, on_failure].compact
37
+ end
38
+
39
+ def generate_edges(all_actions)
40
+ @on_success = all_actions[on_success]
41
+ @on_failure = all_actions[on_failure]
42
+ end
43
+
44
+ def steps=(steps)
45
+ unless steps.last.validator?
46
+ warn "WARNING: Action: #{@name}'s final step is not a validation step!"
47
+ warn "An action can not confirm its success without a validation step!"
48
+ end
49
+ @steps = steps
50
+ end
51
+
52
+ # Again, boilerplate for initial setup
53
+ def perform(bot)
54
+ element = nil
55
+ status = @steps.map do |step|
56
+ if step.validator?
57
+ step.perform(bot, element)
58
+ else
59
+ if step.retain?
60
+ step.perform(bot, element)
61
+ else
62
+ element = step.perform(bot, element)
63
+ end
64
+ nil
65
+ end
66
+ end
67
+ !status.reject(&:nil?).include?(false)
68
+ rescue StandardError => e
69
+ puts e
70
+ return false
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,14 @@
1
+ module WebMinion
2
+ class Bot
3
+ attr_reader :config
4
+ attr_accessor :bot
5
+
6
+ def initialize(config = {})
7
+ @config = config
8
+ end
9
+
10
+ def execute_step(method, target, value = nil, element = nil)
11
+ method(method).call(target, value, element)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,109 @@
1
+ require "mechanize"
2
+ require "web_minion/bots/bot"
3
+
4
+ class MultipleOptionsFoundError < StandardError; end
5
+ class NoInputFound < StandardError; end
6
+ # Mechanize driven bot. More efficient, but can"t handle any dynamic js-driven
7
+ # pages
8
+ module WebMinion
9
+ class MechanizeBot < WebMinion::Bot
10
+ def initialize(config = {})
11
+ super(config)
12
+ @bot = Mechanize.new
13
+ end
14
+
15
+ def page
16
+ @bot.page
17
+ end
18
+
19
+ def body
20
+ page.body.to_s
21
+ end
22
+
23
+ def go(target, _value, _element)
24
+ @bot.get(target)
25
+ end
26
+
27
+ def click(target, _value, _element)
28
+ button = @bot.page.at(target)
29
+ @bot.click(button)
30
+ end
31
+
32
+ def click_button_in_form(target, _value, element)
33
+ element.button_with(target).click
34
+ end
35
+
36
+ def save_page_html(_target, value, _element)
37
+ write_html_file(value)
38
+ end
39
+
40
+ ## FORM METHODS ##
41
+ # Must have an element passed to them (except get form)
42
+
43
+ def get_form_in_index(target, _value, _element)
44
+ @bot.page.forms[target]
45
+ end
46
+
47
+ def get_form(target, _value, _element)
48
+ @bot.page.form_with(target)
49
+ end
50
+
51
+ def get_field(target, _value, element)
52
+ element.field_with(target)
53
+ end
54
+
55
+ def fill_in_input(target, value, element)
56
+ input = element[target]
57
+ raise(NoInputFound, "For target: #{target}") unless input
58
+ element[target] = value
59
+ element
60
+ end
61
+
62
+ # Element should be an instance of a form
63
+ def submit(_target, _value, element)
64
+ @bot.submit element
65
+ end
66
+
67
+ def select_field(target, _value, element)
68
+ options = element.options_with(target)
69
+ raise(MultipleOptionsFoundError, "For target: #{target}") if options.count > 1
70
+ options.first.click
71
+ end
72
+
73
+ def select_checkbox(target, _value, element)
74
+ element.checkbox_with(target).check
75
+ end
76
+
77
+ def select_radio_button(target, _value, element)
78
+ radio = element.radiobutton_with(target)
79
+ radio.checked = true
80
+ radio
81
+ end
82
+
83
+ def select_first_radio_button(_target, _value, element)
84
+ radio = element.radiobuttons.first
85
+ radio.checked = true
86
+ radio
87
+ end
88
+
89
+ ## VALIDATION METHODS ##
90
+
91
+ def url_equals(_target, value, _element)
92
+ !!(@bot.page.uri.to_s == value)
93
+ end
94
+
95
+ def body_includes(_target, value, _element)
96
+ !!(body.index(value) && body.index(value) > 0)
97
+ end
98
+
99
+ def value_equals(_target, value, element)
100
+ !!(element && (element.value == value))
101
+ end
102
+
103
+ private
104
+
105
+ def write_html_file(filename)
106
+ File.open(filename, "w") { |f| f.puts body }
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,35 @@
1
+ module WebMinion
2
+ class CycleChecker
3
+ attr_accessor :checked, :on_stack, :actions, :starting_action, :cyclical
4
+
5
+ def initialize(starting_action)
6
+ @checked = []
7
+ @on_stack = []
8
+ @starting_action = starting_action
9
+ @cyclical = false
10
+ check_for_cycle(starting_action)
11
+ end
12
+
13
+ def cycle?
14
+ @cyclical && !@cyclical.nil?
15
+ end
16
+
17
+ private
18
+
19
+ def check_for_cycle(action)
20
+ @checked << action
21
+ @on_stack << action
22
+
23
+ action.next_actions.each do |act|
24
+ if !@checked.include?(act)
25
+ check_for_cycle(act)
26
+ elsif @on_stack.include?(act)
27
+ @cyclical = true
28
+ return nil
29
+ end
30
+ end
31
+
32
+ @on_stack.delete(action)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,101 @@
1
+ require "json"
2
+ require "web_minion/bots/mechanize_bot"
3
+ require "web_minion/action"
4
+ require "web_minion/cycle_checker"
5
+ require "web_minion/histories/flow_history"
6
+ require "web_minion/histories/action_history"
7
+
8
+ module WebMinion
9
+ # A flow represents the top level watcher of a series of actions that are to
10
+ # be performed. It tracks the sucess or failure, where to go next given an
11
+ # outcome, and a history of all actions performed.
12
+ class Flow
13
+ class NoStartingActionError < StandardError; end
14
+ class CyclicalFlowError < StandardError; end
15
+
16
+ attr_accessor :actions, :bot, :history
17
+ attr_writer :name
18
+ attr_reader :curr_action, :starting_action
19
+
20
+ def initialize(actions, bot, name = "")
21
+ @actions = actions
22
+ @bot = bot
23
+ @name = name
24
+ @history = nil
25
+ end
26
+
27
+ def self.build_via_json(rule_json)
28
+ ruleset = JSON.parse(rule_json)
29
+ bot = MechanizeBot.new(ruleset["config"])
30
+ build_from_hash(ruleset["flow"].merge(bot: bot))
31
+ end
32
+
33
+ def self.build_from_hash(fields = {})
34
+ flow = new([], nil, nil)
35
+ fields.each_pair do |k, v|
36
+ flow.send("#{k}=", v)
37
+ end
38
+ flow
39
+ end
40
+
41
+ def actions=(actions)
42
+ @actions = {}
43
+ actions.each do |act|
44
+ action = Action.build_from_hash(act)
45
+ @actions[action.key] = action
46
+ @starting_action = action if action.starting_action?
47
+ end
48
+
49
+ set_next_actions
50
+ validate_actions
51
+ end
52
+
53
+ def all_actions
54
+ @actions.values
55
+ end
56
+
57
+ def perform
58
+ @history = FlowHistory.new
59
+ status = execute_action(@starting_action)
60
+ @history.end_time = Time.now
61
+ @history.status = status
62
+ @history
63
+ end
64
+
65
+ private
66
+
67
+ def execute_action(action)
68
+ @curr_action = action
69
+ @history.action_history << ActionHistory.new(action.name, action.key)
70
+ status = action.perform(@bot)
71
+ update_action_history(status)
72
+ if status
73
+ action.ending_action? ? true : execute_action(action.on_success)
74
+ else
75
+ action.on_failure ? execute_action(action.on_failure) : false
76
+ end
77
+ end
78
+
79
+ def update_action_history(status)
80
+ @history.action_history.last.end_time = Time.now
81
+ @history.action_history.last.status = status
82
+ end
83
+
84
+ def set_next_actions
85
+ all_actions.each { |act| act.generate_edges(@actions) }
86
+ end
87
+
88
+ def validate_actions
89
+ if all_actions.count(&:starting_action?) == 0
90
+ raise(NoStartingActionError, "Flow: #{@name} has no starting action!")
91
+ end
92
+
93
+ if CycleChecker.new(@starting_action).cycle?
94
+ raise(CyclicalFlowError,
95
+ "Flow: #{@name} is cyclical and could enter an infinite loop")
96
+ end
97
+
98
+ true
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,13 @@
1
+ require "web_minion/histories/action_history"
2
+
3
+ module WebMinion
4
+ class ActionHistory < WebMinion::History
5
+ attr_reader :action_name, :action_key
6
+
7
+ def initialize(action_name, action_key)
8
+ super()
9
+ @action_name = action_name
10
+ @action_key = action_key
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ require "web_minion/histories/history"
2
+
3
+ module WebMinion
4
+ class FlowHistory < WebMinion::History
5
+ attr_reader :action_history
6
+
7
+ def initialize
8
+ super()
9
+ @action_history = []
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,21 @@
1
+ module WebMinion
2
+ # Histories are used to log the events as the bot performs its flows, steps,
3
+ # and actions
4
+ class History
5
+ attr_reader :runtime, :status, :start_time, :end_time
6
+
7
+ def initialize(start_time = nil)
8
+ @start_time = start_time || Time.now
9
+ @status = nil
10
+ end
11
+
12
+ def status=(status)
13
+ @status = status ? "Successful" : "Unsuccessful"
14
+ end
15
+
16
+ def end_time=(end_time)
17
+ @end_time = end_time
18
+ @runtime = @end_time - @start_time if @start_time && @end_time
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,57 @@
1
+ module WebMinion
2
+ class InvalidMethodError < StandardError; end
3
+
4
+ # A Step represents the individual operation that the bot will perform. This
5
+ # often includes grabbing an element from the DOM tree, or performing some
6
+ # operation on an element that has already been found.
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
29
+
30
+ def initialize(fields = {})
31
+ fields.each_pair do |k, v|
32
+ send("#{k}=", v)
33
+ end
34
+ end
35
+
36
+ def perform(bot, element = nil)
37
+ bot.execute_step(@method, @target, @value, element)
38
+ end
39
+
40
+ def method=(method)
41
+ raise(InvalidMethodError, "Method: #{method} is not valid") unless valid_method?(method.to_sym)
42
+ @method = method.to_sym
43
+ end
44
+
45
+ def retain?
46
+ retain_element
47
+ end
48
+
49
+ def validator?
50
+ is_validator
51
+ end
52
+
53
+ def valid_method?(method)
54
+ VALID_METHODS.include?(method)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,3 @@
1
+ module WebMinion
2
+ VERSION = "0.1.0".freeze
3
+ end
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "web_minion/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "web_minion"
8
+ spec.version = WebMinion::VERSION
9
+ spec.authors = ["Andrew Parrish"]
10
+ spec.email = ["m.andrewparrish@gmail.com"]
11
+
12
+ spec.summary = "A metadata-driven browser automation."
13
+ spec.description = "A metadata-driven browser automation."
14
+ spec.homepage = "https://github.com/jibeinc/web_minion"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "bin"
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "minitest", "~> 5.0"
25
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: web_minion
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andrew Parrish
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-06-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ description: A metadata-driven browser automation.
56
+ email:
57
+ - m.andrewparrish@gmail.com
58
+ executables:
59
+ - console
60
+ - setup
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - ".gitignore"
65
+ - ".rubocop.yml"
66
+ - ".travis.yml"
67
+ - CODE_OF_CONDUCT.md
68
+ - Gemfile
69
+ - LICENSE.txt
70
+ - README.md
71
+ - Rakefile
72
+ - bin/console
73
+ - bin/setup
74
+ - lib/jibe_ruleset_bot/version.rb
75
+ - lib/web_minion.rb
76
+ - lib/web_minion/action.rb
77
+ - lib/web_minion/bots/bot.rb
78
+ - lib/web_minion/bots/mechanize_bot.rb
79
+ - lib/web_minion/cycle_checker.rb
80
+ - lib/web_minion/flow.rb
81
+ - lib/web_minion/histories/action_history.rb
82
+ - lib/web_minion/histories/flow_history.rb
83
+ - lib/web_minion/histories/history.rb
84
+ - lib/web_minion/step.rb
85
+ - lib/web_minion/version.rb
86
+ - web_minion.gemspec
87
+ homepage: https://github.com/jibeinc/web_minion
88
+ licenses:
89
+ - MIT
90
+ metadata: {}
91
+ post_install_message:
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ requirements: []
106
+ rubyforge_project:
107
+ rubygems_version: 2.5.1
108
+ signing_key:
109
+ specification_version: 4
110
+ summary: A metadata-driven browser automation.
111
+ test_files: []