howitzer 2.0.0 → 2.0.1

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