howitzer 2.1.1 → 2.2.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.
Files changed (68) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +27 -18
  3. data/.travis.yml +4 -3
  4. data/CHANGELOG.md +15 -1
  5. data/README.md +6 -3
  6. data/Rakefile +1 -1
  7. data/features/cli_new.feature +12 -8
  8. data/features/cli_update.feature +14 -9
  9. data/features/step_definitions/common_steps.rb +3 -3
  10. data/generators/base_generator.rb +1 -1
  11. data/generators/config/config_generator.rb +2 -1
  12. data/generators/config/templates/boot.rb +1 -1
  13. data/generators/config/templates/capybara.rb +2 -1
  14. data/generators/config/templates/default.yml +22 -4
  15. data/generators/config/templates/drivers/appium.rb +25 -0
  16. data/generators/config/templates/drivers/headless_firefox.rb +23 -0
  17. data/generators/cucumber/templates/cuke_sniffer.rake +2 -2
  18. data/generators/cucumber/templates/env.rb +8 -0
  19. data/generators/prerequisites/templates/factory_bot.rb +1 -0
  20. data/generators/root/root_generator.rb +1 -1
  21. data/generators/root/templates/{.rubocop.yml → .rubocop.yml.erb} +34 -13
  22. data/generators/root/templates/Gemfile.erb +4 -0
  23. data/generators/rspec/templates/spec_helper.rb +1 -0
  24. data/generators/turnip/templates/spec_helper.rb +1 -0
  25. data/howitzer.gemspec +3 -3
  26. data/lib/howitzer.rb +40 -0
  27. data/lib/howitzer/cache.rb +19 -18
  28. data/lib/howitzer/capybara_helpers.rb +13 -5
  29. data/lib/howitzer/email.rb +1 -0
  30. data/lib/howitzer/mail_adapters/gmail.rb +3 -0
  31. data/lib/howitzer/mail_adapters/mailgun.rb +2 -0
  32. data/lib/howitzer/mail_adapters/mailtrap.rb +3 -0
  33. data/lib/howitzer/mailgun_api/connector.rb +1 -0
  34. data/lib/howitzer/meta.rb +11 -0
  35. data/lib/howitzer/meta/actions.rb +38 -0
  36. data/lib/howitzer/meta/element.rb +38 -0
  37. data/lib/howitzer/meta/entry.rb +62 -0
  38. data/lib/howitzer/meta/iframe.rb +41 -0
  39. data/lib/howitzer/meta/section.rb +30 -0
  40. data/lib/howitzer/version.rb +1 -1
  41. data/lib/howitzer/web/capybara_context_holder.rb +1 -0
  42. data/lib/howitzer/web/capybara_methods_proxy.rb +4 -1
  43. data/lib/howitzer/web/element_dsl.rb +1 -0
  44. data/lib/howitzer/web/iframe_dsl.rb +2 -0
  45. data/lib/howitzer/web/page.rb +10 -0
  46. data/lib/howitzer/web/page_dsl.rb +3 -0
  47. data/lib/howitzer/web/page_validator.rb +2 -0
  48. data/lib/howitzer/web/section.rb +8 -0
  49. data/lib/howitzer/web/section_dsl.rb +1 -0
  50. data/spec/support/shared_examples/capybara_context_holder.rb +1 -1
  51. data/spec/support/shared_examples/meta_highlight_xpath.rb +41 -0
  52. data/spec/unit/generators/config_generator_spec.rb +4 -2
  53. data/spec/unit/generators/root_generator_spec.rb +32 -21
  54. data/spec/unit/generators/templates/cucumber_spec.rb +97 -0
  55. data/spec/unit/generators/templates/rspec_spec.rb +88 -0
  56. data/spec/unit/generators/templates/turnip_spec.rb +98 -0
  57. data/spec/unit/lib/capybara_helpers_spec.rb +37 -4
  58. data/spec/unit/lib/howitzer_spec.rb +23 -0
  59. data/spec/unit/lib/meta/element_spec.rb +59 -0
  60. data/spec/unit/lib/meta/entry_spec.rb +77 -0
  61. data/spec/unit/lib/meta/iframe_spec.rb +66 -0
  62. data/spec/unit/lib/meta/section_spec.rb +43 -0
  63. data/spec/unit/lib/utils/string_extensions_spec.rb +1 -1
  64. data/spec/unit/lib/web/element_dsl_spec.rb +10 -1
  65. data/spec/unit/lib/web/page_spec.rb +7 -0
  66. data/spec/unit/lib/web/section_spec.rb +7 -0
  67. metadata +31 -15
  68. data/generators/config/templates/drivers/phantomjs.rb +0 -19
@@ -23,6 +23,7 @@ module Howitzer
23
23
  def client(api_key = Howitzer.mailgun_key)
24
24
  check_api_key(api_key)
25
25
  return @client if @api_key == api_key && @api_key
26
+
26
27
  @api_key = api_key
27
28
  @client = Client.new(api_key: @api_key)
28
29
  end
@@ -0,0 +1,11 @@
1
+ module Howitzer
2
+ # This module holds meta-information about elements on the page
3
+ module Meta
4
+ end
5
+ end
6
+
7
+ require 'howitzer/meta/actions'
8
+ require 'howitzer/meta/element'
9
+ require 'howitzer/meta/section'
10
+ require 'howitzer/meta/iframe'
11
+ require 'howitzer/meta/entry'
@@ -0,0 +1,38 @@
1
+ module Howitzer
2
+ module Meta
3
+ # Module with utility actions for elements
4
+ module Actions
5
+ # Highlights element with red border on the page
6
+ # @param args [Array] arguments for elements described with lambda locators and
7
+ # inline options for element/s as a hash
8
+ def highlight(*args)
9
+ if xpath(*args).blank?
10
+ Howitzer::Log.debug("Element #{name} not found on the page")
11
+ return
12
+ end
13
+ context.execute_script("document.evaluate('#{escape(xpath(*args))}', document, null, " \
14
+ 'XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.style.border = "thick solid red"')
15
+ end
16
+
17
+ # Returns xpath for the element
18
+ # @param args [Array] arguments for elements described with lambda locators and
19
+ # inline options for element/s as a hash
20
+ # @return [String, nil]
21
+ def xpath(*args)
22
+ capybara_element(*args).try(:path)
23
+ end
24
+
25
+ private
26
+
27
+ def escape(xpath)
28
+ xpath.gsub(/(['"])/, '\\\\\\1')
29
+ end
30
+
31
+ def convert_args(args)
32
+ new_args = []
33
+ params = args.reject { |v| new_args << v if v.is_a?(Hash) }
34
+ [params, new_args.reduce(:merge)].flatten
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,38 @@
1
+ module Howitzer
2
+ module Meta
3
+ # This class represents element entity within howitzer meta information interface
4
+ class Element
5
+ attr_reader :name, :context
6
+
7
+ include Howitzer::Meta::Actions
8
+ # Creates new meta element with meta information and utility actions
9
+ # @param name [String] name of the element
10
+ # @param context [Howitzer::Web::Page] page element belongs to
11
+ def initialize(name, context)
12
+ @name = name
13
+ @context = context
14
+ end
15
+
16
+ # Finds all instances of element on the page and returns them as array of capybara elements
17
+ # @param args [Array] arguments for elements described with lambda locators and
18
+ # inline options for element/s as a hash
19
+ # @return [Array]
20
+ def capybara_elements(*args)
21
+ context.send("#{name}_elements", *args)
22
+ end
23
+
24
+ # Finds element on the page and returns as a capybara element
25
+ # @param args [Array] arguments for elements described with lambda locators and
26
+ # inline options for element/s as a hash
27
+ # @param wait [Integer] wait time for element search
28
+ # @return [Capybara::Node::Element, nil]
29
+ def capybara_element(*args, wait: 0)
30
+ args << { match: :first, wait: wait }
31
+ args = convert_args(args)
32
+ context.send("#{name}_element", *args)
33
+ rescue Capybara::ElementNotFound
34
+ nil
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,62 @@
1
+ module Howitzer
2
+ module Meta
3
+ # This class provides access to meta information entities
4
+ class Entry
5
+ attr_reader :context
6
+
7
+ # Creates new meta entry instance for the page which provides access to elements, iframes and sections
8
+ # @param context [Howitzer::Web::Page] page for which entry is created
9
+ def initialize(context)
10
+ @context = context
11
+ end
12
+
13
+ # Returns array of elements
14
+ # @return [Array]
15
+ def elements
16
+ @elements ||= context
17
+ .private_methods
18
+ .grep(/\A(?!wait_)\w+_element\z/)
19
+ .map { |el| Meta::Element.new(el.to_s.gsub('_element', ''), context) }
20
+ end
21
+
22
+ # Finds element by name
23
+ # @param name [String, Symbol] element name
24
+ # @return [Meta::Element]
25
+ def element(name)
26
+ elements.find { |el| el.name == name.to_s }
27
+ end
28
+
29
+ # Returns array of sections
30
+ # @return [Array]
31
+ def sections
32
+ @sections ||= context
33
+ .public_methods
34
+ .grep(/\A(?!wait_)\w+_section$\z/)
35
+ .map { |el| Meta::Section.new(el.to_s.gsub('_section', ''), context) }
36
+ end
37
+
38
+ # Finds section by name
39
+ # @param name [String, Symbol] section name
40
+ # @return [Meta::Section]
41
+ def section(name)
42
+ sections.find { |el| el.name == name.to_s }
43
+ end
44
+
45
+ # Returns array of iframes
46
+ # @return [Array]
47
+ def iframes
48
+ @iframes ||= context
49
+ .public_methods
50
+ .grep(/\A(?!wait_)\w+_iframe$\z/)
51
+ .map { |el| Meta::Iframe.new(el.to_s.gsub('_iframe', ''), context) }
52
+ end
53
+
54
+ # Finds iframe by name
55
+ # @param name [String, Symbol] iframe name
56
+ # @return [Meta::Iframe]
57
+ def iframe(name)
58
+ iframes.find { |el| el.name == name.to_s }
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,41 @@
1
+ module Howitzer
2
+ module Meta
3
+ # This class represents iframe entity within howitzer meta information interface
4
+ class Iframe
5
+ attr_reader :name, :context
6
+
7
+ include Howitzer::Meta::Actions
8
+
9
+ # Creates new meta iframe element with meta information and utility actions
10
+ # @param name [String] name of the iframe
11
+ # @param context [Howitzer::Web::Page] page which has this iframe
12
+ def initialize(name, context)
13
+ @name = name
14
+ @context = context
15
+ end
16
+
17
+ # Finds all instances of iframe on the page and returns them as array of capybara elements
18
+ # @return [Array]
19
+ def capybara_elements
20
+ context.capybara_context.all("iframe[src='#{site_value}']")
21
+ end
22
+
23
+ # Finds iframe on the page and returns as a capybara element
24
+ # @param wait [Integer] wait time for element search
25
+ # @return [Capybara::Node::Element, nil]
26
+ def capybara_element(wait: 0)
27
+ context.capybara_context.find("iframe[src='#{site_value}']", match: :first, wait: wait)
28
+ rescue Capybara::ElementNotFound
29
+ nil
30
+ end
31
+
32
+ # Returns url value for iframe
33
+ # @return [String]
34
+ def site_value
35
+ return @site_value if @site_value.present?
36
+
37
+ context.send("#{name}_iframe") { |frame| @site_value = frame.class.send(:site_value) }
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,30 @@
1
+ module Howitzer
2
+ module Meta
3
+ # This class represents section entity within howitzer meta information interface
4
+ class Section
5
+ attr_reader :name, :context
6
+
7
+ include Howitzer::Meta::Actions
8
+ # Creates meta section element with meta information and utility actions
9
+ # @param name [String] name of the section
10
+ # @param context [Howitzer::Web::Page] page which has this section
11
+ def initialize(name, context)
12
+ @name = name
13
+ @context = context
14
+ end
15
+
16
+ # Finds all instances of section on the page and returns them as array of capybara elements
17
+ # @return [Array]
18
+ def capybara_elements
19
+ context.send("#{name}_sections").map(&:capybara_context)
20
+ end
21
+
22
+ # Finds section on the page and returns as a capybara element
23
+ # @return [Capybara::Node::Element, nil]
24
+ def capybara_element
25
+ section = context.send("#{name}_sections").first
26
+ section.try(:capybara_context)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,4 +1,4 @@
1
1
  # This module holds howitzer version
2
2
  module Howitzer
3
- VERSION = '2.1.1'.freeze #:nodoc:
3
+ VERSION = '2.2.0'.freeze #:nodoc:
4
4
  end
@@ -12,6 +12,7 @@ module Howitzer
12
12
 
13
13
  def capybara_scopes
14
14
  return super if defined?(super)
15
+
15
16
  raise NotImplementedError, "Please define 'capybara_scopes' method for class holder"
16
17
  end
17
18
  end
@@ -9,6 +9,7 @@ class Capybara::Selenium::Driver # rubocop:disable Style/ClassAndModuleChildren
9
9
  # https://github.com/teamcapybara/capybara/issues/1845
10
10
  def title
11
11
  return browser.title unless within_frame?
12
+
12
13
  find_xpath('/html/head/title').map { |n| n[:text] }.first.to_s
13
14
  end
14
15
 
@@ -16,6 +17,7 @@ class Capybara::Selenium::Driver # rubocop:disable Style/ClassAndModuleChildren
16
17
  # https://github.com/seleniumhq/selenium/issues/1727
17
18
  def current_url
18
19
  return browser.current_url unless within_frame?
20
+
19
21
  execute_script('return document.location.href')
20
22
  end
21
23
 
@@ -57,7 +59,8 @@ module Howitzer
57
59
  private
58
60
 
59
61
  def capybara_scopes
60
- @_scopes ||= [Capybara.current_session]
62
+ @capybara_scopes ||= Hash.new { |hash, key| hash[key] = [Capybara.current_session] }
63
+ @capybara_scopes[Howitzer.session_name]
61
64
  end
62
65
  end
63
66
  end
@@ -15,6 +15,7 @@ module Howitzer
15
15
  args, params, options = merge_element_options(args, params)
16
16
  args = args.map do |el|
17
17
  next(el) unless el.is_a?(Proc)
18
+
18
19
  el.call(*params.shift(el.arity))
19
20
  end
20
21
  args << options unless options.blank?
@@ -84,8 +84,10 @@ module Howitzer
84
84
 
85
85
  def iframe(name, *args)
86
86
  raise ArgumentError, 'iframe selector arguments must be specified' if args.blank?
87
+
87
88
  klass = args.first.is_a?(Class) ? args.shift : find_matching_class(name)
88
89
  raise NameError, "class can not be found for #{name} iframe" if klass.blank?
90
+
89
91
  define_iframe(klass, name, args)
90
92
  define_has_iframe(name, args)
91
93
  define_has_no_iframe(name, args)
@@ -1,6 +1,7 @@
1
1
  require 'singleton'
2
2
  require 'rspec/expectations'
3
3
  require 'addressable/template'
4
+ require 'howitzer/meta'
4
5
  require 'howitzer/web/capybara_methods_proxy'
5
6
  require 'howitzer/web/page_validator'
6
7
  require 'howitzer/web/element_dsl'
@@ -65,6 +66,7 @@ module Howitzer
65
66
  page_list = matched_pages
66
67
  return UnknownPage if page_list.count.zero?
67
68
  return page_list.first if page_list.count == 1
69
+
68
70
  raise Howitzer::AmbiguousPageMatchingError, ambiguous_page_msg(page_list)
69
71
  end
70
72
 
@@ -77,6 +79,7 @@ module Howitzer
77
79
  end_time = ::Time.now + timeout
78
80
  until ::Time.now > end_time
79
81
  return true if opened?
82
+
80
83
  sleep(0.5)
81
84
  end
82
85
  raise Howitzer::IncorrectPageError, incorrect_page_msg
@@ -98,9 +101,16 @@ module Howitzer
98
101
  if defined?(path_value)
99
102
  return "#{site_value}#{Addressable::Template.new(path_value).expand(params, url_processor)}"
100
103
  end
104
+
101
105
  raise Howitzer::NoPathForPageError, "Please specify path for '#{self}' page. Example: path '/home'"
102
106
  end
103
107
 
108
+ # Provides access to meta information about entities on the page
109
+ # @return [Meta::Entry]
110
+ def meta
111
+ @meta ||= Meta::Entry.new(self)
112
+ end
113
+
104
114
  class << self
105
115
  protected
106
116
 
@@ -32,6 +32,7 @@ module Howitzer
32
32
  def method_missing(name, *args, &block)
33
33
  return super if name =~ /\A(?:be|have)_/
34
34
  return eval_in_out_context(*args, &block) if name == :out
35
+
35
36
  page_klass.given.send(name, *args, &block)
36
37
  end
37
38
 
@@ -46,8 +47,10 @@ module Howitzer
46
47
 
47
48
  def eval_in_out_context(*args, &block)
48
49
  return nil if args.size.zero?
50
+
49
51
  name = args.shift
50
52
  return get_outer_instance_variable(name) if name.to_s.start_with?('@')
53
+
51
54
  outer_context.send(name, *args, &block)
52
55
  end
53
56
 
@@ -21,6 +21,7 @@ module Howitzer
21
21
 
22
22
  def check_validations_are_defined!
23
23
  return if self.class.validations.present?
24
+
24
25
  raise Howitzer::NoValidationError, "No any page validation was found for '#{self.class.name}' page"
25
26
  end
26
27
 
@@ -60,6 +61,7 @@ module Howitzer
60
61
 
61
62
  def opened?(sync: true)
62
63
  return validations.all? { |(_, validation)| validation.call(self, sync) } if validations.present?
64
+
63
65
  raise Howitzer::NoValidationError, "No any page validation was found for '#{name}' page"
64
66
  end
65
67
 
@@ -1,9 +1,16 @@
1
1
  require 'howitzer/web/base_section'
2
+ require 'howitzer/meta'
2
3
 
3
4
  module Howitzer
4
5
  module Web
5
6
  # This class uses for named sections which possible to reuse in different pages
6
7
  class Section < BaseSection
8
+ # Provides access to meta information about entities in section
9
+ # @return [Meta::Entry]
10
+ def meta
11
+ @meta ||= Meta::Entry.new(self)
12
+ end
13
+
7
14
  class << self
8
15
  protected
9
16
 
@@ -19,6 +26,7 @@ module Howitzer
19
26
 
20
27
  def me(*args)
21
28
  raise ArgumentError, 'Finder arguments are missing' if args.blank?
29
+
22
30
  @default_finder_args = args
23
31
  end
24
32
  end
@@ -50,6 +50,7 @@ module Howitzer
50
50
  def finder_args
51
51
  @finder_args ||= begin
52
52
  return @args if @args.present?
53
+
53
54
  section_class.default_finder_args || raise(ArgumentError, 'Missing finder arguments')
54
55
  end
55
56
  end
@@ -13,7 +13,7 @@ RSpec.shared_examples :capybara_context_holder do
13
13
  before do
14
14
  web_page_class.class_eval do
15
15
  def capybara_scopes
16
- @_scopes ||= [true]
16
+ @capybara_scopes ||= [true]
17
17
  end
18
18
  end
19
19
  end
@@ -0,0 +1,41 @@
1
+ RSpec.shared_examples :meta_highlight_xpath do
2
+ let(:element) { described_class.new(name, context) }
3
+ describe '#xpath' do
4
+ let(:capybara_element) { double }
5
+ context 'when element is found' do
6
+ before { allow(element).to receive(:capybara_element) { capybara_element } }
7
+ it do
8
+ expect(capybara_element).to receive(:path)
9
+ element.xpath
10
+ end
11
+ end
12
+ context 'when element is blank' do
13
+ before { allow(element).to receive(:capybara_element) {} }
14
+ it do
15
+ expect(capybara_element).not_to receive(:path)
16
+ element.xpath
17
+ end
18
+ end
19
+ end
20
+
21
+ describe '#highlight' do
22
+ context 'when xpath blank' do
23
+ before { allow(element).to receive(:xpath) { nil } }
24
+ it do
25
+ expect(Howitzer::Log).to receive(:debug).with("Element #{name} not found on the page")
26
+ expect(context).not_to receive(:execute_script)
27
+ element.highlight
28
+ end
29
+ end
30
+ context 'when xpath is present' do
31
+ before { allow(element).to receive(:xpath) { '//a' } }
32
+ it do
33
+ expect(context).to receive(:execute_script).with(
34
+ "document.evaluate('//a', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE,"\
35
+ ' null).singleNodeValue.style.border = "thick solid red"'
36
+ )
37
+ element.highlight
38
+ end
39
+ end
40
+ end
41
+ end