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.
- checksums.yaml +5 -5
- data/.rubocop.yml +27 -18
- data/.travis.yml +4 -3
- data/CHANGELOG.md +15 -1
- data/README.md +6 -3
- data/Rakefile +1 -1
- data/features/cli_new.feature +12 -8
- data/features/cli_update.feature +14 -9
- data/features/step_definitions/common_steps.rb +3 -3
- data/generators/base_generator.rb +1 -1
- data/generators/config/config_generator.rb +2 -1
- data/generators/config/templates/boot.rb +1 -1
- data/generators/config/templates/capybara.rb +2 -1
- data/generators/config/templates/default.yml +22 -4
- data/generators/config/templates/drivers/appium.rb +25 -0
- data/generators/config/templates/drivers/headless_firefox.rb +23 -0
- data/generators/cucumber/templates/cuke_sniffer.rake +2 -2
- data/generators/cucumber/templates/env.rb +8 -0
- data/generators/prerequisites/templates/factory_bot.rb +1 -0
- data/generators/root/root_generator.rb +1 -1
- data/generators/root/templates/{.rubocop.yml → .rubocop.yml.erb} +34 -13
- data/generators/root/templates/Gemfile.erb +4 -0
- data/generators/rspec/templates/spec_helper.rb +1 -0
- data/generators/turnip/templates/spec_helper.rb +1 -0
- data/howitzer.gemspec +3 -3
- data/lib/howitzer.rb +40 -0
- data/lib/howitzer/cache.rb +19 -18
- data/lib/howitzer/capybara_helpers.rb +13 -5
- data/lib/howitzer/email.rb +1 -0
- data/lib/howitzer/mail_adapters/gmail.rb +3 -0
- data/lib/howitzer/mail_adapters/mailgun.rb +2 -0
- data/lib/howitzer/mail_adapters/mailtrap.rb +3 -0
- data/lib/howitzer/mailgun_api/connector.rb +1 -0
- data/lib/howitzer/meta.rb +11 -0
- data/lib/howitzer/meta/actions.rb +38 -0
- data/lib/howitzer/meta/element.rb +38 -0
- data/lib/howitzer/meta/entry.rb +62 -0
- data/lib/howitzer/meta/iframe.rb +41 -0
- data/lib/howitzer/meta/section.rb +30 -0
- data/lib/howitzer/version.rb +1 -1
- data/lib/howitzer/web/capybara_context_holder.rb +1 -0
- data/lib/howitzer/web/capybara_methods_proxy.rb +4 -1
- data/lib/howitzer/web/element_dsl.rb +1 -0
- data/lib/howitzer/web/iframe_dsl.rb +2 -0
- data/lib/howitzer/web/page.rb +10 -0
- data/lib/howitzer/web/page_dsl.rb +3 -0
- data/lib/howitzer/web/page_validator.rb +2 -0
- data/lib/howitzer/web/section.rb +8 -0
- data/lib/howitzer/web/section_dsl.rb +1 -0
- data/spec/support/shared_examples/capybara_context_holder.rb +1 -1
- data/spec/support/shared_examples/meta_highlight_xpath.rb +41 -0
- data/spec/unit/generators/config_generator_spec.rb +4 -2
- data/spec/unit/generators/root_generator_spec.rb +32 -21
- data/spec/unit/generators/templates/cucumber_spec.rb +97 -0
- data/spec/unit/generators/templates/rspec_spec.rb +88 -0
- data/spec/unit/generators/templates/turnip_spec.rb +98 -0
- data/spec/unit/lib/capybara_helpers_spec.rb +37 -4
- data/spec/unit/lib/howitzer_spec.rb +23 -0
- data/spec/unit/lib/meta/element_spec.rb +59 -0
- data/spec/unit/lib/meta/entry_spec.rb +77 -0
- data/spec/unit/lib/meta/iframe_spec.rb +66 -0
- data/spec/unit/lib/meta/section_spec.rb +43 -0
- data/spec/unit/lib/utils/string_extensions_spec.rb +1 -1
- data/spec/unit/lib/web/element_dsl_spec.rb +10 -1
- data/spec/unit/lib/web/page_spec.rb +7 -0
- data/spec/unit/lib/web/section_spec.rb +7 -0
- metadata +31 -15
- data/generators/config/templates/drivers/phantomjs.rb +0 -19
@@ -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
|
data/lib/howitzer/version.rb
CHANGED
@@ -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
|
-
@
|
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
|
@@ -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)
|
data/lib/howitzer/web/page.rb
CHANGED
@@ -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
|
|
data/lib/howitzer/web/section.rb
CHANGED
@@ -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
|
@@ -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
|