howitzer 0.0.3 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|