howitzer 2.0.0 → 2.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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +21 -1
  4. data/features/cli_new.feature +1 -0
  5. data/features/cli_update.feature +1 -0
  6. data/generators/base_generator.rb +3 -1
  7. data/generators/config/templates/capybara.rb +1 -1
  8. data/generators/config/templates/default.yml +4 -2
  9. data/generators/cucumber/cucumber_generator.rb +1 -0
  10. data/generators/cucumber/templates/common_steps.rb +1 -1
  11. data/generators/cucumber/templates/env.rb +0 -28
  12. data/generators/cucumber/templates/hooks.rb +29 -0
  13. data/generators/prerequisites/templates/user.rb +2 -0
  14. data/generators/prerequisites/templates/users.rb +2 -2
  15. data/generators/root/templates/Gemfile.erb +1 -1
  16. data/generators/rspec/templates/spec_helper.rb +9 -2
  17. data/generators/turnip/templates/spec_helper.rb +9 -2
  18. data/generators/web/templates/example_page.rb +0 -1
  19. data/howitzer.gemspec +1 -3
  20. data/lib/howitzer.rb +7 -0
  21. data/lib/howitzer/cache.rb +1 -1
  22. data/lib/howitzer/capybara_helpers.rb +2 -2
  23. data/lib/howitzer/email.rb +26 -8
  24. data/lib/howitzer/exceptions.rb +19 -19
  25. data/lib/howitzer/mail_adapters/abstract.rb +4 -3
  26. data/lib/howitzer/mail_adapters/mailgun.rb +6 -5
  27. data/lib/howitzer/mailgun_api/client.rb +1 -1
  28. data/lib/howitzer/version.rb +1 -1
  29. data/lib/howitzer/web/base_section.rb +3 -3
  30. data/lib/howitzer/web/capybara_context_holder.rb +19 -0
  31. data/lib/howitzer/web/capybara_methods_proxy.rb +27 -19
  32. data/lib/howitzer/web/element_dsl.rb +52 -13
  33. data/lib/howitzer/web/iframe_dsl.rb +3 -7
  34. data/lib/howitzer/web/page.rb +14 -21
  35. data/lib/howitzer/web/page_dsl.rb +32 -4
  36. data/lib/howitzer/web/page_validator.rb +15 -16
  37. data/lib/howitzer/web/section_dsl.rb +3 -7
  38. data/spec/config/custom.yml +1 -1
  39. data/spec/spec_helper.rb +1 -7
  40. data/spec/support/shared_examples/capybara_context_holder.rb +3 -3
  41. data/spec/support/shared_examples/element_dsl.rb +128 -18
  42. data/spec/unit/generators/base_generator_spec.rb +15 -16
  43. data/spec/unit/generators/cucumber_generator_spec.rb +2 -0
  44. data/spec/unit/generators/root_generator_spec.rb +1 -1
  45. data/spec/unit/lib/capybara_helpers_spec.rb +2 -2
  46. data/spec/unit/lib/email_spec.rb +37 -6
  47. data/spec/unit/lib/{howitzer.rb → howitzer_spec.rb} +9 -0
  48. data/spec/unit/lib/mail_adapters/abstract_spec.rb +1 -1
  49. data/spec/unit/lib/mail_adapters/mailgun_spec.rb +4 -4
  50. data/spec/unit/lib/web/base_section_spec.rb +3 -1
  51. data/spec/unit/lib/web/element_dsl_spec.rb +7 -2
  52. data/spec/unit/lib/web/page_dsl_spec.rb +22 -0
  53. data/spec/unit/lib/web/page_spec.rb +79 -44
  54. data/spec/unit/lib/web/page_validator_spec.rb +94 -51
  55. data/spec/unit/lib/web/section_spec.rb +4 -2
  56. metadata +10 -8
@@ -17,10 +17,11 @@ module Howitzer
17
17
  attr_reader :message
18
18
 
19
19
  # Finds an email in mailbox
20
- # @param recipient [String] an email
21
- # @param subject [String]
20
+ # @param _recipient [String] an email
21
+ # @param _subject [String]
22
+ # @param _wait [Integer] how much time is required to wait an email
22
23
 
23
- def self.find(_recipient, _subject)
24
+ def self.find(_recipient, _subject, _wait:)
24
25
  raise NotImplementedError
25
26
  end
26
27
 
@@ -10,11 +10,12 @@ module Howitzer
10
10
  # @note emails are stored for 3 days only!
11
11
  # @param recipient [String] an email
12
12
  # @param subject [String]
13
- # @raise [EmailNotFoundError] if message blank
13
+ # @param wait [Integer] how much time is required to wait an email
14
+ # @raise [EmailNotFoundError] if blank message
14
15
 
15
- def self.find(recipient, subject)
16
+ def self.find(recipient, subject, wait:)
16
17
  message = {}
17
- retryable(find_retry_params) { message = retrieve_message(recipient, subject) }
18
+ retryable(find_retry_params(wait)) { message = retrieve_message(recipient, subject) }
18
19
  return new(message) if message.present?
19
20
  raise Howitzer::EmailNotFoundError,
20
21
  "Message with subject '#{subject}' for recipient '#{recipient}' was not found."
@@ -91,9 +92,9 @@ module Howitzer
91
92
  end
92
93
  private_class_method :event_by
93
94
 
94
- def self.find_retry_params
95
+ def self.find_retry_params(wait)
95
96
  {
96
- timeout: Howitzer.mailgun_idle_timeout,
97
+ timeout: wait || Howitzer.try(:mailgun_idle_timeout),
97
98
  sleep: Howitzer.mailgun_sleep_time,
98
99
  silent: true,
99
100
  logger: Howitzer::Log,
@@ -9,7 +9,7 @@ module Howitzer
9
9
  # wrapper around RestClient so you don't have to worry about the HTTP aspect
10
10
  # of communicating with Mailgun API.
11
11
  class Client
12
- USER_AGENT = 'mailgun-sdk-ruby'.freeze
12
+ USER_AGENT = 'mailgun-sdk-ruby'.freeze #:nodoc:
13
13
  attr_reader :api_user, :api_key, :api_host, :api_version, :ssl
14
14
  def initialize(api_user: 'api', api_key:, api_host: 'api.mailgun.net', api_version: 'v3', ssl: true)
15
15
  @api_user = api_user
@@ -1,4 +1,4 @@
1
1
  # This module holds howitzer version
2
2
  module Howitzer
3
- VERSION = '2.0.0'.freeze
3
+ VERSION = '2.0.1'.freeze #:nodoc:
4
4
  end
@@ -7,12 +7,12 @@ module Howitzer
7
7
  module Web
8
8
  # This class holds base functinality for sections
9
9
  class BaseSection
10
- include CapybaraMethodsProxy
11
10
  include ElementDsl
12
11
  include SectionDsl
13
12
  include IframeDsl
13
+ include CapybaraMethodsProxy
14
14
 
15
- attr_reader :parent, :capybara_context
15
+ attr_reader :parent
16
16
 
17
17
  class << self
18
18
  attr_reader :default_finder_args
@@ -20,7 +20,7 @@ module Howitzer
20
20
 
21
21
  def initialize(parent, context)
22
22
  @parent = parent
23
- @capybara_context = context
23
+ capybara_scopes << context
24
24
  end
25
25
  end
26
26
  end
@@ -0,0 +1,19 @@
1
+ module Howitzer
2
+ module Web
3
+ # This module mixin capybara context methods
4
+ module CapybaraContextHolder
5
+ # Returns capybara context. For example, capybara session, parent element, etc.
6
+
7
+ def capybara_context
8
+ capybara_scopes.last
9
+ end
10
+
11
+ private
12
+
13
+ def capybara_scopes
14
+ return super if defined?(super)
15
+ raise NotImplementedError, "Please define 'capybara_scopes' method for class holder"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,28 +1,36 @@
1
1
  require 'capybara'
2
2
 
3
3
  module Howitzer
4
- # This module proxies required original capybara methods to recipient
5
- module CapybaraMethodsProxy
6
- PROXIED_CAPYBARA_METHODS = Capybara::Session::SESSION_METHODS +
7
- Capybara::Session::MODAL_METHODS +
8
- [:driver, :text]
4
+ module Web
5
+ # This module proxies required original capybara methods to recipient
6
+ module CapybaraMethodsProxy
7
+ PROXIED_CAPYBARA_METHODS = Capybara::Session::SESSION_METHODS + #:nodoc:
8
+ Capybara::Session::MODAL_METHODS +
9
+ [:driver, :text]
9
10
 
10
- # Capybara form dsl methods are not compatible with page object pattern and Howitzer gem.
11
- # Instead of including Capybara::DSL module, we proxy most interesting Capybara methods and
12
- # prevent using extra methods which can potentially broke main principles and framework concept
13
- PROXIED_CAPYBARA_METHODS.each do |method|
14
- define_method(method) { |*args, &block| Capybara.current_session.send(method, *args, &block) }
15
- end
11
+ # Capybara form dsl methods are not compatible with page object pattern and Howitzer gem.
12
+ # Instead of including Capybara::DSL module, we proxy most interesting Capybara methods and
13
+ # prevent using extra methods which can potentially broke main principles and framework concept
14
+ PROXIED_CAPYBARA_METHODS.each do |method|
15
+ define_method(method) { |*args, &block| Capybara.current_session.send(method, *args, &block) }
16
+ end
17
+
18
+ # Accepts or declines JS alert box by given flag
19
+ # @param flag [Boolean] Determines accept or decline alert box
20
+
21
+ def click_alert_box(flag)
22
+ if %w(selenium sauce).include? Howitzer.driver
23
+ alert = driver.browser.switch_to.alert
24
+ flag ? alert.accept : alert.dismiss
25
+ else
26
+ evaluate_script("window.confirm = function() { return #{flag}; }")
27
+ end
28
+ end
16
29
 
17
- # Accepts or declines JS alert box by given flag
18
- # @param flag [Boolean] Determines accept or decline alert box
30
+ private
19
31
 
20
- def click_alert_box(flag)
21
- if %w(selenium sauce).include? Howitzer.driver
22
- alert = driver.browser.switch_to.alert
23
- flag ? alert.accept : alert.dismiss
24
- else
25
- evaluate_script("window.confirm = function() { return #{flag}; }")
32
+ def capybara_scopes
33
+ @_scopes ||= [Capybara.current_session]
26
34
  end
27
35
  end
28
36
  end
@@ -1,24 +1,24 @@
1
+ require 'howitzer/web/capybara_context_holder'
1
2
  module Howitzer
2
3
  module Web
3
4
  # This module combines element dsl methods
4
5
  module ElementDsl
6
+ include CapybaraContextHolder
7
+
5
8
  def self.included(base) #:nodoc:
6
9
  base.extend(ClassMethods)
7
10
  end
8
11
 
9
- # Returns capybara context. For example, capybara session, parent element, etc.
10
- # @abstract should be defined in parent context
11
-
12
- def capybara_context
13
- raise NotImplementedError, "Please define 'capybara_context' method for class holder"
14
- end
15
-
16
12
  private
17
13
 
18
14
  def convert_arguments(args, params)
19
- args.map do |arg|
20
- arg.is_a?(Proc) ? arg.call(*params) : arg
15
+ hash = params.deep_dup.pop if params.last.is_a?(Hash)
16
+ args = args.map do |el|
17
+ next(el) unless el.is_a?(Proc)
18
+ el.call(*params.shift(el.arity))
21
19
  end
20
+ args << hash unless hash.nil?
21
+ args
22
22
  end
23
23
 
24
24
  # This module holds element dsl methods methods
@@ -34,6 +34,10 @@ module Howitzer
34
34
  #
35
35
  # <b><em>element_name</em>_elements.first</b> - equals capybara #first(...) method
36
36
  #
37
+ # <b>wait_for_<em>element_name</em>_element</b> - equals capybara #find(...) method but returns nil
38
+ #
39
+ # <b>within_<em>element_name</em>_element</b> - equals capybara #within(...) method
40
+ #
37
41
  # <b>has_<em>element_name</em>_element?</b> - equals capybara #has_selector(...) method
38
42
  #
39
43
  # <b>has_no_<em>element_name</em>_element?</b> - equals capybara #has_no_selector(...) method
@@ -41,15 +45,28 @@ module Howitzer
41
45
  # @param args [Array] original Capybara arguments. For details, see `Capybara::Node::Finders#all.
42
46
  # @example Using in a page class
43
47
  # class HomePage < Howitzer::Web::Page
48
+ # element :top_panel, '.top'
49
+ # element :bottom_panel, '.bottom'
44
50
  # element :new_button, :xpath, ".//*[@name='New']"
45
51
  #
46
- # def press_new_button
47
- # new_button_element.click
52
+ # def press_top_new_button
53
+ # within_top_panel_element do
54
+ # new_button_element.click
55
+ # end
56
+ # end
57
+ #
58
+ # def press_bottom_new_button
59
+ # within_bottom_panel_element do
60
+ # new_button_element.click
61
+ # end
48
62
  # end
49
63
  # end
50
64
  #
51
- # HomePage.on { is_expected.to have_new_button_element }
52
- # HomePage.on { is_expected.to have_no_new_button_element }
65
+ # HomePage.on do
66
+ # is_expected.to have_top_panel_element
67
+ # press_top_new_element
68
+ # is_expected.to have_no_new_button_element(match: :first)
69
+ # end
53
70
  # @example Using in a section class
54
71
  # class MenuSection < Howitzer::Web::Section
55
72
  # me '.main-menu'
@@ -66,6 +83,8 @@ module Howitzer
66
83
  validate_arguments!(args)
67
84
  define_element(name, args)
68
85
  define_elements(name, args)
86
+ define_wait_for_element(name, args)
87
+ define_within_element(name, args)
69
88
  define_has_element(name, args)
70
89
  define_has_no_element(name, args)
71
90
  end
@@ -92,6 +111,26 @@ module Howitzer
92
111
  private "#{name}_elements"
93
112
  end
94
113
 
114
+ def define_wait_for_element(name, args)
115
+ define_method("wait_for_#{name}_element") do |*block_args|
116
+ capybara_context.find(*convert_arguments(args, block_args))
117
+ return nil
118
+ end
119
+ private "wait_for_#{name}_element"
120
+ end
121
+
122
+ def define_within_element(name, args)
123
+ define_method("within_#{name}_element") do |*block_args, &block|
124
+ new_scope = capybara_context.find(*convert_arguments(args, block_args))
125
+ begin
126
+ capybara_scopes.push(new_scope)
127
+ block.call
128
+ ensure
129
+ capybara_scopes.pop
130
+ end
131
+ end
132
+ end
133
+
95
134
  def define_has_element(name, args)
96
135
  define_method("has_#{name}_element?") do |*block_args|
97
136
  capybara_context.has_selector?(*convert_arguments(args, block_args))
@@ -1,18 +1,14 @@
1
+ require 'howitzer/web/capybara_context_holder'
1
2
  module Howitzer
2
3
  module Web
3
4
  # This module combines iframe dsl methods
4
5
  module IframeDsl
6
+ include CapybaraContextHolder
7
+
5
8
  def self.included(base) #:nodoc:
6
9
  base.extend(ClassMethods)
7
10
  end
8
11
 
9
- # Returns capybara context. For example, capybara session, parent element, etc.
10
- # @abstract should be defined in parent context
11
-
12
- def capybara_context
13
- raise NotImplementedError, "Please define 'capybara_context' method for class holder"
14
- end
15
-
16
12
  private
17
13
 
18
14
  def iframe_element_selector(selector)
@@ -1,5 +1,4 @@
1
1
  require 'singleton'
2
- require 'capybara'
3
2
  require 'rspec/expectations'
4
3
  require 'addressable/template'
5
4
  require 'howitzer/web/capybara_methods_proxy'
@@ -14,7 +13,7 @@ module Howitzer
14
13
  module Web
15
14
  # This class represents a single web page. This is a parent class for all web pages
16
15
  class Page
17
- UnknownPage = Class.new
16
+ UnknownPage = Class.new #:nodoc:
18
17
  include Singleton
19
18
  include CapybaraMethodsProxy
20
19
  include ElementDsl
@@ -25,22 +24,20 @@ module Howitzer
25
24
  include ::RSpec::Matchers
26
25
 
27
26
  # This Ruby callback makes all inherited classes as singleton classes.
28
- # In additional it addes current page to page validator pages in case
29
- # if it has any defined validations.
30
27
 
31
28
  def self.inherited(subclass)
32
29
  subclass.class_eval { include Singleton }
33
- PageValidator.pages << subclass if subclass.validations.present?
34
30
  end
35
31
 
36
32
  # Opens a web page in browser
37
33
  # @note It tries to open the page twice and then raises the error if a validation is failed
38
34
  # @param validate [Boolean] if fase will skip current page validation (is opened)
35
+ # @param url_processor [Class] custom url processor. For details see 'addressable' gem
39
36
  # @param params [Array] placeholder names and their values
40
37
  # @return [Page]
41
38
 
42
- def self.open(validate: true, **params)
43
- url = expanded_url(params)
39
+ def self.open(validate: true, url_processor: nil, **params)
40
+ url = expanded_url(params, url_processor)
44
41
  Howitzer::Log.info "Open #{name} page by '#{url}' url"
45
42
  retryable(tries: 2, logger: Howitzer::Log, trace: true, on: Exception) do |retries|
46
43
  Howitzer::Log.info 'Retry...' unless retries.zero?
@@ -70,7 +67,7 @@ module Howitzer
70
67
  end
71
68
 
72
69
  # Waits until a web page is opened
73
- # @param time_out [Integer] time in seconds a required web page to be loaded
70
+ # @param timeout [Integer] time in seconds a required web page to be loaded
74
71
  # @return [Boolean]
75
72
  # @raise [IncorrectPageError] when timeout expired and the page is not displayed
76
73
 
@@ -91,11 +88,14 @@ module Howitzer
91
88
 
92
89
  # Returns an expanded page url for the page opening
93
90
  # @param params [Array] placeholders and their values
91
+ # @param url_processor [Class] custom url processor. For details see Addressable gem
94
92
  # @return [String]
95
93
  # @raise [NoPathForPageError] if an url is not specified for the page
96
94
 
97
- def self.expanded_url(params = {})
98
- return "#{app_host}#{Addressable::Template.new(path_template).expand(params)}" unless path_template.nil?
95
+ def self.expanded_url(params = {}, url_processor = nil)
96
+ if defined?(path_value)
97
+ return "#{site_value}#{Addressable::Template.new(path_value).expand(params, url_processor)}"
98
+ end
99
99
  raise Howitzer::NoPathForPageError, "Please specify path for '#{self}' page. Example: path '/home'"
100
100
  end
101
101
 
@@ -113,7 +113,8 @@ module Howitzer
113
113
  # @!visibility public
114
114
 
115
115
  def path(value)
116
- @path_template = value.to_s
116
+ define_singleton_method(:path_value) { value.to_s }
117
+ private_class_method :path_value
117
118
  end
118
119
 
119
120
  # DSL to specify a site for the page opening
@@ -130,14 +131,12 @@ module Howitzer
130
131
  # @!visibility public
131
132
 
132
133
  def site(value)
133
- define_singleton_method(:app_host) { value }
134
- private_class_method :app_host
134
+ define_singleton_method(:site_value) { value }
135
+ private_class_method :site_value
135
136
  end
136
137
 
137
138
  private
138
139
 
139
- attr_reader :path_template
140
-
141
140
  def incorrect_page_msg
142
141
  "Current page: #{current_page}, expected: #{self}.\n" \
143
142
  "\tCurrent url: #{current_url}\n\tCurrent title: #{instance.title}"
@@ -162,12 +161,6 @@ module Howitzer
162
161
  Howitzer::Log.info "Reload '#{current_url}'"
163
162
  visit current_url
164
163
  end
165
-
166
- # Returns capybara context as current session
167
-
168
- def capybara_context
169
- Capybara.current_session
170
- end
171
164
  end
172
165
  end
173
166
  end
@@ -8,6 +8,7 @@ module Howitzer
8
8
 
9
9
  def initialize(page_klass, &block)
10
10
  self.page_klass = page_klass
11
+ self.outer_context = eval('self', block.binding) if block.present?
11
12
  instance_eval(&block)
12
13
  end
13
14
 
@@ -20,10 +21,16 @@ module Howitzer
20
21
  expect(page_klass.given)
21
22
  end
22
23
 
23
- # Proxies all methods to page instance except methods with be_ and have_ prefixes
24
+ # Proxies all methods to a page instance.
25
+ #
26
+ # @note There are some exceptions:
27
+ # * Methods with `be_` and `have_` prefixes are excluded
28
+ # * `out` method extracts an instance variable from an original context if starts from @.
29
+ # Otherwise it executes a method from an original context
24
30
 
25
31
  def method_missing(name, *args, &block)
26
32
  return super if name =~ /\A(?:be|have)_/
33
+ return eval_in_out_context(*args, &block) if name == :out
27
34
  page_klass.given.send(name, *args, &block)
28
35
  end
29
36
 
@@ -36,10 +43,21 @@ module Howitzer
36
43
 
37
44
  private
38
45
 
39
- attr_accessor :page_klass
46
+ def eval_in_out_context(*args, &block)
47
+ return nil if args.size.zero?
48
+ name = args.shift
49
+ return get_outer_instance_variable(name) if name.to_s.start_with?('@')
50
+ outer_context.send(name, *args, &block)
51
+ end
52
+
53
+ def get_outer_instance_variable(name)
54
+ outer_context.instance_variable_get(name)
55
+ end
56
+
57
+ attr_accessor :page_klass, :outer_context
40
58
  end
41
59
 
42
- def self.included(base)
60
+ def self.included(base) #:nodoc:
43
61
  base.extend(ClassMethods)
44
62
  end
45
63
  # This module holds page dsl class methods
@@ -47,12 +65,22 @@ module Howitzer
47
65
  # Allows to execute page methods in context of the page.
48
66
  # @note It additionally checks the page is really displayed
49
67
  # on each method call, otherwise it raises error
50
- # @example
68
+ # @example Standard case
51
69
  # LoginPage.open
52
70
  # LoginPage.on do
53
71
  # fill_form(name: 'John', email: 'jkarpensky@gmail.com')
54
72
  # submit_form
55
73
  # end
74
+ # @example More complex case with outer context
75
+ # @name = 'John'
76
+ # def email(domain = 'gmail.com')
77
+ # "jkarpensky@#{domain}"
78
+ # end
79
+ # LoginPage.open
80
+ # LoginPage.on do
81
+ # fill_form(name: out(:@name), email: out(:email, 'yahoo.com'))
82
+ # submit_form
83
+ # end
56
84
 
57
85
  def on(&block)
58
86
  PageScope.new(self, &block)