howitzer 2.1.1 → 2.2.0

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