howitzer 0.0.3 → 1.0.1
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.
- data/.gitignore +8 -0
- data/.rspec +3 -0
- data/.travis.yml +4 -0
- data/CHANGELOG.md +32 -0
- data/GETTING_STARTED.md +529 -0
- data/Gemfile +4 -2
- data/LICENSE +2 -2
- data/README.md +57 -13
- data/Rakefile +9 -2
- data/bin/howitzer +79 -31
- data/generators/base_generator.rb +87 -0
- data/generators/config/config_generator.rb +16 -20
- data/generators/config/templates/default.yml +26 -7
- data/generators/cucumber/cucumber_generator.rb +20 -26
- data/generators/{tasks → cucumber}/templates/cucumber.rake +0 -0
- data/generators/{config → cucumber}/templates/cucumber.yml +0 -0
- data/generators/emails/emails_generator.rb +11 -18
- data/generators/emails/templates/example_email.rb +1 -0
- data/generators/pages/pages_generator.rb +16 -18
- data/generators/pages/templates/example_menu.rb +1 -0
- data/generators/pages/templates/example_page.rb +1 -1
- data/generators/root/root_generator.rb +18 -20
- data/generators/root/templates/.gitignore +2 -1
- data/generators/root/templates/Gemfile +3 -2
- data/generators/root/templates/Rakefile +10 -0
- data/generators/root/templates/boot.rb +3 -9
- data/generators/rspec/rspec_generator.rb +23 -0
- data/generators/rspec/templates/example_spec.rb +7 -0
- data/generators/rspec/templates/rspec.rake +34 -0
- data/generators/rspec/templates/spec_helper.rb +56 -0
- data/generators/tasks/tasks_generator.rb +11 -17
- data/generators/tasks/templates/common.rake +15 -0
- data/howitzer.gemspec +13 -2
- data/lib/howitzer.rb +1 -0
- data/lib/howitzer/helpers.rb +87 -2
- data/lib/howitzer/init.rb +0 -1
- data/lib/howitzer/patches/rawler_patched.rb +86 -0
- data/lib/howitzer/settings.rb +27 -0
- data/lib/howitzer/utils.rb +3 -12
- data/lib/howitzer/utils/capybara_patched.rb +3 -2
- data/lib/howitzer/utils/capybara_settings.rb +158 -24
- data/lib/howitzer/utils/data_generator/data_storage.rb +35 -1
- data/lib/howitzer/utils/data_generator/gen.rb +45 -3
- data/lib/howitzer/utils/email/email.rb +44 -5
- data/lib/howitzer/utils/email/mail_client.rb +28 -22
- data/lib/howitzer/utils/email/mailgun_helper.rb +30 -4
- data/lib/howitzer/utils/locator_store.rb +111 -19
- data/lib/howitzer/utils/log.rb +137 -0
- data/lib/howitzer/utils/page_validator.rb +86 -0
- data/lib/howitzer/vendor/firebug-1.12.1-fx.xpi +0 -0
- data/lib/howitzer/vendor/firepath-0.9.7-fx.xpi +0 -0
- data/lib/howitzer/version.rb +2 -2
- data/lib/howitzer/web_page.rb +159 -19
- data/spec/active_resource.rb +0 -0
- data/spec/config/custom.yml +1 -0
- data/spec/config/default.yml +28 -0
- data/spec/spec_helper.rb +46 -1
- data/spec/support/boot_helper.rb +15 -0
- data/spec/support/generator_helper.rb +13 -0
- data/spec/support/logger_helper.rb +12 -0
- data/spec/unit/bin/howitzer_spec.rb +175 -0
- data/spec/unit/generators/generators_spec.rb +175 -0
- data/spec/unit/lib/capybara_settings_spec.rb +170 -0
- data/spec/unit/lib/helpers_spec.rb +619 -0
- data/spec/unit/lib/init_spec.rb +14 -0
- data/spec/unit/lib/settings_spec.rb +17 -0
- data/spec/unit/lib/utils/data_generator/data_storage_spec.rb +62 -0
- data/spec/unit/lib/utils/data_generator/gen_spec.rb +151 -0
- data/spec/unit/lib/utils/email/email_spec.rb +75 -0
- data/spec/unit/lib/utils/email/mail_client_spec.rb +115 -0
- data/spec/unit/lib/utils/email/mailgun_helper_spec.rb +95 -0
- data/spec/unit/lib/utils/locator_store_spec.rb +122 -0
- data/spec/unit/lib/utils/log_spec.rb +107 -0
- data/spec/unit/lib/utils/page_validator_spec.rb +142 -0
- data/spec/unit/lib/web_page_spec.rb +250 -0
- data/spec/unit/version_spec.rb +8 -0
- metadata +215 -15
- data/Gemfile.lock +0 -103
- data/History.md +0 -20
- data/lib/howitzer/utils/logger.rb +0 -108
- data/spec/howitzer/version_spec.rb +0 -8
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'log4r'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module Howitzer
|
5
|
+
class Log
|
6
|
+
include Singleton
|
7
|
+
include Log4r
|
8
|
+
|
9
|
+
[:debug, :info, :warn, :fatal].each do |method_name|
|
10
|
+
define_method method_name do |text|
|
11
|
+
@logger.send(method_name, text)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
#
|
17
|
+
# Prints log entry about error with ERROR severity
|
18
|
+
# *Examples:*
|
19
|
+
# log.error MyException, 'Some error text', caller
|
20
|
+
# log.error 'Some error text', caller
|
21
|
+
# log.error MyException, 'Some caller text'
|
22
|
+
# log.error 'Some error text'
|
23
|
+
# log.error err_object
|
24
|
+
#
|
25
|
+
# *Parameters:*
|
26
|
+
# * +args+ - see example
|
27
|
+
#
|
28
|
+
|
29
|
+
def error(*args)
|
30
|
+
object = if args.first.nil?
|
31
|
+
$!
|
32
|
+
else
|
33
|
+
case args.size
|
34
|
+
when 1
|
35
|
+
args.first.is_a?(Exception) ? args.first : RuntimeError.new(args.first)
|
36
|
+
when 2
|
37
|
+
if args.first.is_a?(Class) && args.first < Exception
|
38
|
+
args.first.new(args.last)
|
39
|
+
else
|
40
|
+
exception = RuntimeError.new(args.first)
|
41
|
+
exception.set_backtrace(args.last)
|
42
|
+
exception
|
43
|
+
end
|
44
|
+
when 3
|
45
|
+
exception = args.first.new(args[1])
|
46
|
+
exception.set_backtrace(args.last)
|
47
|
+
exception
|
48
|
+
else nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
err_backtrace = object.backtrace ? "\n\t#{object.backtrace.join("\n\t")}" : nil
|
52
|
+
@logger.error("[#{object.class}] #{object.message}#{err_backtrace}")
|
53
|
+
fail(object)
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
#
|
58
|
+
# Prints feature name into log with INFO severity
|
59
|
+
#
|
60
|
+
# *Parameters:*
|
61
|
+
# * +text+ - Feature name
|
62
|
+
#
|
63
|
+
|
64
|
+
def print_feature_name(text)
|
65
|
+
log_without_formatting{ info "*** Feature: #{text.upcase} ***" }
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
#
|
70
|
+
# Returns formatted howitzer settings
|
71
|
+
#
|
72
|
+
|
73
|
+
def settings_as_formatted_text
|
74
|
+
log_without_formatting{ info settings.as_formatted_text }
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
#
|
79
|
+
# Prints scenario name into log with INFO severity
|
80
|
+
#
|
81
|
+
# *Parameters:*
|
82
|
+
# * +text+ - Scenario name
|
83
|
+
#
|
84
|
+
def print_scenario_name(text)
|
85
|
+
log_without_formatting{ info " => Scenario: #{text}" }
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def initialize
|
91
|
+
@logger = Logger.new("ruby_log")
|
92
|
+
@logger.add(console_log)
|
93
|
+
@logger.add(error_log)
|
94
|
+
@logger.add(txt_log)
|
95
|
+
self.base_formatter = default_formatter
|
96
|
+
Logger["ruby_log"].level = settings.debug_mode ? ALL : INFO
|
97
|
+
Logger["ruby_log"].trace = true
|
98
|
+
end
|
99
|
+
|
100
|
+
def log_without_formatting
|
101
|
+
self.base_formatter = blank_formatter
|
102
|
+
yield
|
103
|
+
self.base_formatter = default_formatter
|
104
|
+
end
|
105
|
+
|
106
|
+
def console_log
|
107
|
+
StdoutOutputter.new(:console).tap{|o| o.only_at(INFO, DEBUG, WARN)}
|
108
|
+
end
|
109
|
+
|
110
|
+
def error_log
|
111
|
+
StderrOutputter.new(:error, 'level' => ERROR)
|
112
|
+
end
|
113
|
+
|
114
|
+
def txt_log
|
115
|
+
FileUtils.mkdir_p(settings.log_dir) unless File.exists?(settings.log_dir)
|
116
|
+
filename = File.join(settings.log_dir, settings.txt_log)
|
117
|
+
FileOutputter.new(:txt_log, :filename => filename, :trunc => true)
|
118
|
+
end
|
119
|
+
|
120
|
+
def blank_formatter
|
121
|
+
PatternFormatter.new(:pattern => "%m")
|
122
|
+
end
|
123
|
+
|
124
|
+
def default_formatter
|
125
|
+
if settings.hide_datetime_from_log
|
126
|
+
params = {pattern: "[%l] %m"}
|
127
|
+
else
|
128
|
+
params = {pattern: "%d [%l] :: %m", date_pattern: "%Y/%m/%d %H:%M:%S"}
|
129
|
+
end
|
130
|
+
PatternFormatter.new(params)
|
131
|
+
end
|
132
|
+
|
133
|
+
def base_formatter=(formatter)
|
134
|
+
@logger.outputters.each {|outputter| outputter.formatter = formatter}
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Howitzer
|
2
|
+
module Utils
|
3
|
+
module PageValidator
|
4
|
+
WrongOptionError = Class.new(StandardError)
|
5
|
+
NoValidationError = Class.new(StandardError)
|
6
|
+
UnknownValidationName = Class.new(StandardError)
|
7
|
+
@validations = {}
|
8
|
+
|
9
|
+
##
|
10
|
+
#
|
11
|
+
# Returns validation list
|
12
|
+
#
|
13
|
+
# @return [Hash]
|
14
|
+
#
|
15
|
+
def self.validations
|
16
|
+
@validations
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.included(base) #:nodoc:
|
20
|
+
base.extend(ClassMethods)
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
#
|
25
|
+
# Checks that correct page has been loaded
|
26
|
+
#
|
27
|
+
# @raise [Howitzer::Utils::PageValidator::NoValidationError] If no validation was specified
|
28
|
+
#
|
29
|
+
def check_correct_page_loaded
|
30
|
+
validations = PageValidator.validations[self.class.name]
|
31
|
+
raise NoValidationError, "No any page validation was found" if validations.nil?
|
32
|
+
validations.each {|(_, validation)| validation.call(self)}
|
33
|
+
end
|
34
|
+
|
35
|
+
module ClassMethods
|
36
|
+
|
37
|
+
##
|
38
|
+
#
|
39
|
+
# Adds validation to validation list
|
40
|
+
#
|
41
|
+
# @param [Symbol or String] name Which validation type. Possible values [:url, :element_presence, :title]
|
42
|
+
# @option options [Hash] Validation options
|
43
|
+
# :pattern => [Regexp] For :url and :title validation types
|
44
|
+
# :locator => [String] For :element_presence (Existing locator name)
|
45
|
+
# @raise [Howitzer::Utils::PageValidator::UnknownValidationName] If unknown validation type was passed
|
46
|
+
#
|
47
|
+
def validates(name, options)
|
48
|
+
raise TypeError, "Expected options to be Hash, actual is '#{options.class}'" unless options.class == Hash
|
49
|
+
PageValidator.validations[self.name] ||= {}
|
50
|
+
case name.to_sym
|
51
|
+
when :url
|
52
|
+
validate_url options
|
53
|
+
when :element_presence
|
54
|
+
validate_element options
|
55
|
+
when :title
|
56
|
+
validate_title options
|
57
|
+
else
|
58
|
+
raise UnknownValidationName, "unknown '#{name}' validation name"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def validate_url(options)
|
65
|
+
pattern = options[:pattern] || options["pattern"]
|
66
|
+
raise WrongOptionError, "Please specify ':pattern' option as Regexp object" if pattern.nil? || !pattern.is_a?(Regexp)
|
67
|
+
PageValidator.validations[self.name][:url] = lambda { |web_page| web_page.wait_for_url(pattern) }
|
68
|
+
end
|
69
|
+
|
70
|
+
def validate_element(options)
|
71
|
+
locator = options[:locator] || options["locator"]
|
72
|
+
raise WrongOptionError, "Please specify ':locator' option as one of page locator names" if locator.nil? || locator.empty?
|
73
|
+
PageValidator.validations[self.name][:element_presence] = lambda { |web_page| web_page.find_element(locator) }
|
74
|
+
end
|
75
|
+
|
76
|
+
def validate_title(options)
|
77
|
+
pattern = options[:pattern] || options["pattern"]
|
78
|
+
raise WrongOptionError, "Please specify ':pattern' option as Regexp object" if pattern.nil? || !pattern.is_a?(Regexp)
|
79
|
+
PageValidator.validations[self.name][:title] = lambda { |web_page| web_page.wait_for_title(pattern) }
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
Binary file
|
Binary file
|
data/lib/howitzer/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
1
|
module Howitzer
|
2
|
-
VERSION = "
|
3
|
-
end
|
2
|
+
VERSION = "1.0.1"
|
3
|
+
end
|
data/lib/howitzer/web_page.rb
CHANGED
@@ -1,62 +1,202 @@
|
|
1
1
|
require "rspec/expectations"
|
2
|
-
require
|
2
|
+
require "howitzer/utils/locator_store"
|
3
|
+
require "howitzer/utils/page_validator"
|
4
|
+
require "singleton"
|
3
5
|
|
4
6
|
class WebPage
|
5
7
|
|
6
8
|
BLANK_PAGE = 'about:blank'
|
9
|
+
IncorrectPageError = Class.new(StandardError)
|
7
10
|
|
8
|
-
#TODO include Capybara DSL here
|
9
11
|
include LocatorStore
|
12
|
+
include Howitzer::Utils::PageValidator
|
10
13
|
include RSpec::Matchers
|
14
|
+
include Capybara::DSL
|
15
|
+
extend Capybara::DSL
|
16
|
+
include Singleton
|
11
17
|
|
12
|
-
def self.
|
18
|
+
def self.inherited(subclass)
|
19
|
+
subclass.class_eval { include Singleton }
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
#
|
24
|
+
# Opens web-site by given url
|
25
|
+
#
|
26
|
+
# *Parameters:*
|
27
|
+
# * +url+ - Url string that will be opened
|
28
|
+
#
|
29
|
+
# *Returns:*
|
30
|
+
# * +WebPage+ - New instance of current class
|
31
|
+
#
|
32
|
+
|
33
|
+
def self.open(url="#{app_url}#{self::URL}")
|
13
34
|
log.info "Open #{self.name} page by '#{url}' url"
|
14
35
|
retryable(tries: 2, logger: log, trace: true, on: Exception) do |retries|
|
15
36
|
log.info "Retry..." unless retries.zero?
|
16
37
|
visit url
|
17
38
|
end
|
18
|
-
|
39
|
+
given
|
19
40
|
end
|
20
41
|
|
42
|
+
##
|
43
|
+
#
|
44
|
+
# Returns singleton instance of current web page
|
45
|
+
#
|
46
|
+
# *Returns:*
|
47
|
+
# * +WebPage+ - Singleton instance
|
48
|
+
#
|
49
|
+
|
21
50
|
def self.given
|
22
|
-
|
51
|
+
self.instance.tap{ |page| page.check_correct_page_loaded }
|
23
52
|
end
|
24
53
|
|
54
|
+
##
|
55
|
+
#
|
56
|
+
# Fills in field that using Tinymce API
|
57
|
+
#
|
58
|
+
# *Parameters:*
|
59
|
+
# * +name+ - Frame name that contains Tinymce field
|
60
|
+
# * +Hash+ - Not required options
|
61
|
+
#
|
62
|
+
|
63
|
+
def tinymce_fill_in(name, options = {})
|
64
|
+
if %w[selenium selenium_dev sauce].include? settings.driver
|
65
|
+
page.driver.browser.switch_to.frame("#{name}_ifr")
|
66
|
+
page.find(:css, '#tinymce').native.send_keys(options[:with])
|
67
|
+
page.driver.browser.switch_to.default_content
|
68
|
+
else
|
69
|
+
page.execute_script("tinyMCE.get('#{name}').setContent('#{options[:with]}')")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
#
|
75
|
+
# Accepts or declines JS alert box by given flag
|
76
|
+
#
|
77
|
+
# *Parameters:*
|
78
|
+
# * +flag+ [TrueClass,FalseClass] - Determines accept or decline alert box
|
79
|
+
#
|
80
|
+
|
81
|
+
def click_alert_box(flag)
|
82
|
+
if %w[selenium selenium_dev sauce].include? settings.driver
|
83
|
+
if flag
|
84
|
+
page.driver.browser.switch_to.alert.accept
|
85
|
+
else
|
86
|
+
page.driver.browser.switch_to.alert.dismiss
|
87
|
+
end
|
88
|
+
else
|
89
|
+
if flag
|
90
|
+
page.evaluate_script('window.confirm = function() { return true; }')
|
91
|
+
else
|
92
|
+
page.evaluate_script('window.confirm = function() { return false; }')
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
##
|
98
|
+
#
|
99
|
+
# Clicks on button or link using JS event call
|
100
|
+
#
|
101
|
+
# *Parameters:*
|
102
|
+
# * +css_locator+ - Css locator of link or button
|
103
|
+
#
|
104
|
+
|
105
|
+
def js_click(css_locator)
|
106
|
+
page.execute_script("$('#{css_locator}').trigger('click')")
|
107
|
+
sleep settings.timeout_tiny
|
108
|
+
end
|
109
|
+
|
110
|
+
# @deprecated
|
111
|
+
# With Capybara 2.x it is extra
|
25
112
|
def wait_for_ajax(timeout=settings.timeout_small, message=nil)
|
26
113
|
end_time = ::Time.now + timeout
|
27
114
|
until ::Time.now > end_time
|
28
|
-
return if page.evaluate_script('$.active') == 0
|
115
|
+
return true if page.evaluate_script('$.active') == 0
|
29
116
|
sleep 0.25
|
30
|
-
log.info "#{Time.now}"
|
31
117
|
end
|
32
118
|
log.error message || "Timed out waiting for ajax requests to complete"
|
33
|
-
|
34
119
|
end
|
35
120
|
|
36
|
-
|
37
|
-
|
121
|
+
##
|
122
|
+
#
|
123
|
+
# Waits until web page is loaded
|
124
|
+
#
|
125
|
+
# *Parameters:*
|
126
|
+
# * +expected_url+ - Url that will be waiting for
|
127
|
+
# * +time_out+ - Seconds that will be waiting for web-site to be loaded until raise error
|
128
|
+
#
|
129
|
+
|
130
|
+
def wait_for_url(expected_url, timeout=settings.timeout_small)
|
131
|
+
end_time = ::Time.now + timeout
|
132
|
+
until ::Time.now > end_time
|
38
133
|
operator = expected_url.is_a?(Regexp) ? :=~ : :==
|
39
|
-
current_url.send(operator, expected_url).tap{|res| sleep 1 unless res}
|
134
|
+
return true if current_url.send(operator, expected_url).tap{|res| sleep 1 unless res}
|
135
|
+
end
|
136
|
+
log.error IncorrectPageError, "Current url: #{current_url}, expected: #{expected_url}"
|
137
|
+
end
|
138
|
+
|
139
|
+
##
|
140
|
+
#
|
141
|
+
# Waits until web is loaded with expected title
|
142
|
+
#
|
143
|
+
# *Parameters:*
|
144
|
+
# * +expected_title+ - Page title that will be waited for
|
145
|
+
# * +time_out+ - Seconds that will be waiting for web-site to be loaded until raise error
|
146
|
+
#
|
147
|
+
|
148
|
+
def wait_for_title(expected_title, timeout=settings.timeout_small)
|
149
|
+
end_time = ::Time.now + timeout
|
150
|
+
until ::Time.now > end_time
|
151
|
+
operator = expected_title.is_a?(Regexp) ? :=~ : :==
|
152
|
+
return true if title.send(operator, expected_title).tap{|res| sleep 1 unless res}
|
40
153
|
end
|
41
|
-
|
42
|
-
log.error "Current url: #{current_url}, expected: #{expected_url}"
|
154
|
+
log.error IncorrectPageError, "Current title: #{title}, expected: #{expected_title}"
|
43
155
|
end
|
44
156
|
|
157
|
+
##
|
158
|
+
#
|
159
|
+
# Reloads current page
|
160
|
+
#
|
161
|
+
|
45
162
|
def reload
|
46
163
|
log.info "Reload '#{current_url}'"
|
47
164
|
visit current_url
|
48
165
|
end
|
49
166
|
|
167
|
+
##
|
168
|
+
#
|
169
|
+
# Returns current url
|
170
|
+
#
|
171
|
+
# *Returns:*
|
172
|
+
# * +string+ - Current url
|
173
|
+
#
|
174
|
+
|
50
175
|
def self.current_url
|
51
176
|
page.current_url
|
52
177
|
end
|
53
178
|
|
54
|
-
|
55
|
-
|
179
|
+
##
|
180
|
+
#
|
181
|
+
# Returns Page title
|
182
|
+
#
|
183
|
+
# *Returns:*
|
184
|
+
# * +string+ - Page title
|
185
|
+
#
|
186
|
+
|
187
|
+
def title
|
188
|
+
page.title
|
56
189
|
end
|
57
190
|
|
58
|
-
|
59
|
-
|
60
|
-
|
191
|
+
##
|
192
|
+
#
|
193
|
+
# Returns body text of html page
|
194
|
+
#
|
195
|
+
# *Returns:*
|
196
|
+
# * +string+ - Body text
|
197
|
+
#
|
198
|
+
|
199
|
+
def self.text
|
200
|
+
page.find('body').text
|
61
201
|
end
|
62
|
-
end
|
202
|
+
end
|