capybara_test_helpers 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +3 -0
- data/README.md +401 -0
- data/lib/capybara_test_helpers.rb +6 -0
- data/lib/capybara_test_helpers/actions.rb +116 -0
- data/lib/capybara_test_helpers/assertions.rb +124 -0
- data/lib/capybara_test_helpers/benchmark_helpers.rb +94 -0
- data/lib/capybara_test_helpers/config.rb +56 -0
- data/lib/capybara_test_helpers/cucumber.rb +12 -0
- data/lib/capybara_test_helpers/dependency_injection.rb +44 -0
- data/lib/capybara_test_helpers/finders.rb +40 -0
- data/lib/capybara_test_helpers/matchers.rb +55 -0
- data/lib/capybara_test_helpers/rspec.rb +33 -0
- data/lib/capybara_test_helpers/selectors.rb +174 -0
- data/lib/capybara_test_helpers/synchronization.rb +41 -0
- data/lib/capybara_test_helpers/test_helper.rb +182 -0
- data/lib/capybara_test_helpers/to_or_expectation_handler.rb +27 -0
- data/lib/capybara_test_helpers/version.rb +6 -0
- data/lib/generators/test_helper/test_helper_generator.rb +26 -0
- metadata +92 -0
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'capybara_test_helpers/to_or_expectation_handler'
|
4
|
+
|
5
|
+
# Internal: Wraps RSpec matchers to allow using them after calling `should` or
|
6
|
+
# `should_not` to perform the assertion.
|
7
|
+
module CapybaraTestHelpers::Assertions
|
8
|
+
# Public: Allows writing custom on-demand matchers, as well as chaining
|
9
|
+
# several assertions.
|
10
|
+
def should(negated = false)
|
11
|
+
negated = !!negated # Coerce to boolean.
|
12
|
+
return self if negated == @negated
|
13
|
+
|
14
|
+
clone.tap { |test_helper| test_helper.instance_variable_set('@negated', negated) }
|
15
|
+
end
|
16
|
+
[:should_still, :should_now, :and, :and_instead, :and_also, :and_still].each { |should_alias| alias_method should_alias, :should }
|
17
|
+
|
18
|
+
# Public: Allows writing custom on-demand matchers, as well as chaining
|
19
|
+
# several assertions.
|
20
|
+
def should_not
|
21
|
+
@negated ? self : should(true)
|
22
|
+
end
|
23
|
+
[:should_still_not, :should_no_longer, :nor, :and_not].each { |should_alias| alias_method should_alias, :should_not }
|
24
|
+
|
25
|
+
# Public: Makes it more readable when in used in combination with to_or.
|
26
|
+
def not_to
|
27
|
+
raise(ArgumentError, 'You must call `should` or `should_not` before calling this method') if @negated.nil?
|
28
|
+
|
29
|
+
@negated
|
30
|
+
end
|
31
|
+
alias or_should_not not_to
|
32
|
+
|
33
|
+
# Public: Allows to write complex nested assertions.
|
34
|
+
def invert_expectation
|
35
|
+
should(!not_to)
|
36
|
+
end
|
37
|
+
|
38
|
+
%i[
|
39
|
+
have_selector
|
40
|
+
have_no_selector
|
41
|
+
have_css
|
42
|
+
have_no_css
|
43
|
+
have_xpath
|
44
|
+
have_no_xpath
|
45
|
+
have_link
|
46
|
+
have_no_link
|
47
|
+
have_button
|
48
|
+
have_no_button
|
49
|
+
have_field
|
50
|
+
have_no_field
|
51
|
+
have_select
|
52
|
+
have_no_select
|
53
|
+
have_table
|
54
|
+
have_no_table
|
55
|
+
have_checked_field
|
56
|
+
have_no_checked_field
|
57
|
+
have_unchecked_field
|
58
|
+
have_no_unchecked_field
|
59
|
+
have_all_of_selectors
|
60
|
+
have_none_of_selectors
|
61
|
+
have_any_of_selectors
|
62
|
+
have_title
|
63
|
+
have_no_title
|
64
|
+
].each do |method_name|
|
65
|
+
CapybaraTestHelpers.define_helper_method(self, method_name, assertion: true)
|
66
|
+
end
|
67
|
+
|
68
|
+
%i[
|
69
|
+
have_ancestor
|
70
|
+
have_no_ancestor
|
71
|
+
have_sibling
|
72
|
+
have_no_sibling
|
73
|
+
match_selector
|
74
|
+
not_match_selector
|
75
|
+
match_css
|
76
|
+
not_match_css
|
77
|
+
match_xpath
|
78
|
+
not_match_xpath
|
79
|
+
have_text
|
80
|
+
have_no_text
|
81
|
+
have_content
|
82
|
+
have_no_content
|
83
|
+
match_style
|
84
|
+
have_style
|
85
|
+
].each do |method_name|
|
86
|
+
CapybaraTestHelpers.define_helper_method(self, method_name, target: :to_capybara_node, assertion: true)
|
87
|
+
end
|
88
|
+
|
89
|
+
%i[
|
90
|
+
have_current_path
|
91
|
+
have_no_current_path
|
92
|
+
].each do |method_name|
|
93
|
+
CapybaraTestHelpers.define_helper_method(self, method_name, target: :page, assertion: true, inject_test_helper: false)
|
94
|
+
end
|
95
|
+
|
96
|
+
alias have have_selector
|
97
|
+
|
98
|
+
# Public: Allows to check on any input value asynchronously.
|
99
|
+
def have_value(expected_value, **options)
|
100
|
+
synchronize_expectation(**options) { expect(value).to_or not_to, eq(expected_value) }
|
101
|
+
self
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
# Internal: Override the method_missing defined in RSpec::Matchers to avoid
|
107
|
+
# accidentally calling a predicate or has matcher instead of an assertion.
|
108
|
+
def method_missing(method, *args, **kwargs, &block)
|
109
|
+
case method.to_s
|
110
|
+
when CapybaraTestHelpers::TestHelper::DYNAMIC_MATCHER_REGEX
|
111
|
+
raise NoMethodError, "undefined method `#{ method }' for #{ inspect }.\nUse `delegate_to_test_context(:#{ method })` in the test helper class if you plan to use it often, or `test_context.#{ method }` as needed in the instance."
|
112
|
+
else
|
113
|
+
super
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Internal: Override the method_missing defined in RSpec::Matchers to avoid
|
118
|
+
# accidentally calling a predicate or has matcher instead of an assertion.
|
119
|
+
def respond_to_missing?(method, *)
|
120
|
+
return false if method =~ CapybaraTestHelpers::TestHelper::DYNAMIC_MATCHER_REGEX
|
121
|
+
|
122
|
+
super
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
require 'active_support/core_ext/numeric/time'
|
5
|
+
require 'rainbow'
|
6
|
+
|
7
|
+
begin
|
8
|
+
require 'amazing_print'
|
9
|
+
rescue LoadError
|
10
|
+
end
|
11
|
+
|
12
|
+
# rubocop:disable Style/ClassVars
|
13
|
+
|
14
|
+
# Public: Keeps track of the running time for user-defined helpers, useful as a
|
15
|
+
# way to keep track of the executed methods, and to easily spot slow operations.
|
16
|
+
module BenchmarkHelpers
|
17
|
+
extend ActiveSupport::Concern
|
18
|
+
|
19
|
+
included do
|
20
|
+
@@indentation_level = 0
|
21
|
+
@@indented_logs = []
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
# Internal: Helper to benchmark an operation, outputs the method name, its
|
27
|
+
# arguments, and the ellapsed time in milliseconds.
|
28
|
+
def benchmark_method(method_name, args, kwargs)
|
29
|
+
@@indented_logs.push(log = +'') # Push it in order, set the content later.
|
30
|
+
@@indentation_level += 1
|
31
|
+
before = Time.now
|
32
|
+
yield
|
33
|
+
ensure
|
34
|
+
diff_in_millis = (Time.now - before).in_milliseconds.round
|
35
|
+
@@indentation_level -= 1
|
36
|
+
|
37
|
+
# Set the queued message with the method call and the ellapsed time.
|
38
|
+
log.sub!('', _benchmark_str(method_name: method_name, args: args, kwargs: kwargs, time: diff_in_millis))
|
39
|
+
|
40
|
+
# Print the messages once we outdent all, and clear the queue.
|
41
|
+
@@indented_logs.each { |inner_log| Kernel.puts(inner_log) }.clear if @@indentation_level.zero?
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# Internal: Indents nested method calls, and adds color to make it readable.
|
47
|
+
def _benchmark_str(method_name:, args:, kwargs:, time:)
|
48
|
+
args_str = args.map(&:inspect)
|
49
|
+
unless kwargs.empty?
|
50
|
+
args_str.push kwargs.respond_to?(:awesome_inspect) ? kwargs.awesome_inspect(multiline: false, ruby19_syntax: true)[2..-3] : kwargs.inspect
|
51
|
+
end
|
52
|
+
[
|
53
|
+
' ' * @@indentation_level,
|
54
|
+
Rainbow(self.class.name.chomp('TestHelper') + '#').slategray.rjust(40),
|
55
|
+
Rainbow(method_name.to_s).cyan,
|
56
|
+
Rainbow("(#{ args_str.join(', ') })").slategray,
|
57
|
+
' ',
|
58
|
+
Rainbow("#{ time } ms").send(time > 1000 && :red || time > 100 && :yellow || :green),
|
59
|
+
].join('')
|
60
|
+
end
|
61
|
+
|
62
|
+
module ClassMethods
|
63
|
+
# Hook: Benchmarks all methods in the class once it's loaded.
|
64
|
+
def on_test_helper_load
|
65
|
+
super
|
66
|
+
benchmark_all
|
67
|
+
end
|
68
|
+
|
69
|
+
# Debug: Wraps all instance methods of the test helper class to log them.
|
70
|
+
def benchmark_all
|
71
|
+
return if defined?(@benchmarked_all)
|
72
|
+
|
73
|
+
benchmark(instance_methods - superclass.instance_methods - [:lazy_for])
|
74
|
+
@benchmarked_all = true
|
75
|
+
end
|
76
|
+
|
77
|
+
# Debug: Wraps a method to output its parameters and ellapsed time.
|
78
|
+
#
|
79
|
+
# Usage:
|
80
|
+
# benchmark :input_for
|
81
|
+
# benchmark def input_for(...)
|
82
|
+
def benchmark(method_names)
|
83
|
+
prepend(Module.new {
|
84
|
+
Array.wrap(method_names).each do |method_name|
|
85
|
+
define_method(method_name) { |*args, **kwargs, &block|
|
86
|
+
benchmark_method(method_name, args, kwargs) { super(*args, **kwargs, &block) }
|
87
|
+
}
|
88
|
+
end
|
89
|
+
})
|
90
|
+
method_names
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
# rubocop:enable Style/ClassVars
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'capybara/rspec'
|
4
|
+
|
5
|
+
# Internal: Configuration for Provides the basic functionality to create simple test helpers.
|
6
|
+
module CapybaraTestHelpers
|
7
|
+
DEFAULTS = {
|
8
|
+
helpers_paths: ['test_helpers'].freeze,
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
# Internal: Reserved methods for Capybara::TestHelper.
|
12
|
+
test_helper_methods = [
|
13
|
+
:page,
|
14
|
+
:find_element,
|
15
|
+
:should,
|
16
|
+
:should_not,
|
17
|
+
:not_to,
|
18
|
+
].freeze
|
19
|
+
|
20
|
+
# Internal: Methods that are in the Capybara DSL but are so common that we
|
21
|
+
# don't want to issue a warning if they are used as selectors.
|
22
|
+
SKIPPED_DSL_METHODS = [
|
23
|
+
:title,
|
24
|
+
:body,
|
25
|
+
:html,
|
26
|
+
].freeze
|
27
|
+
|
28
|
+
# Internal: Methods that should not be overiden or used as locator aliases to
|
29
|
+
# avoid confusion while working on test helpers.
|
30
|
+
RESERVED_METHODS = (Capybara::Session::DSL_METHODS - SKIPPED_DSL_METHODS + test_helper_methods).to_set.freeze
|
31
|
+
|
32
|
+
# Internal: Ruby 2.7 swallows keyword arguments, so for methods that take a
|
33
|
+
# Hash as the first argument as well as keyword arguments, we need to manually
|
34
|
+
# detect and move them to args if empty.
|
35
|
+
METHODS_EXPECTING_A_HASH = %i[matches_style? has_style? match_style have_style].to_set.freeze
|
36
|
+
|
37
|
+
# Public: Returns the current configuration for the test helpers.
|
38
|
+
def self.config
|
39
|
+
@config ||= OpenStruct.new(DEFAULTS)
|
40
|
+
yield @config if block_given?
|
41
|
+
@config
|
42
|
+
end
|
43
|
+
|
44
|
+
# Internal: Allows to define methods that are a part of the Capybara DSL, as
|
45
|
+
# well as RSpec matchers.
|
46
|
+
def self.define_helper_method(klass, method_name, wrap: false, assertion: false, target: 'current_context', return_self: assertion, inject_test_helper: true)
|
47
|
+
klass.class_eval <<~HELPER, __FILE__, __LINE__ + 1
|
48
|
+
def #{ method_name }(*args, **kwargs, &filter)
|
49
|
+
#{ 'args.push(kwargs) && (kwargs = {}) if args.empty?' if METHODS_EXPECTING_A_HASH.include?(method_name) }
|
50
|
+
#{ 'kwargs[:test_helper] = self' if inject_test_helper }
|
51
|
+
#{ 'wrap_element ' if wrap }#{ assertion ? "expect(#{ target }).to_or not_to, test_context" : target }.#{ method_name }(*args, **kwargs, &filter)
|
52
|
+
#{ 'self' if return_self }
|
53
|
+
end
|
54
|
+
HELPER
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'capybara_test_helpers'
|
4
|
+
|
5
|
+
World(CapybaraTestHelpers::DependencyInjection)
|
6
|
+
|
7
|
+
# Public: Use outside of the steps to make it available on all steps.
|
8
|
+
def use_test_helpers(*names)
|
9
|
+
names.each do |name|
|
10
|
+
define_method(name) { get_test_helper(name) }
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Internal: Provides dependency injection for RSpec and Cucumber, by using the
|
4
|
+
# test helper name and a convention for naming and organizing helpers.
|
5
|
+
module CapybaraTestHelpers::DependencyInjection
|
6
|
+
# Public: Returns an instance of a test helper that inherits BaseTestHelper.
|
7
|
+
#
|
8
|
+
# NOTE: Memoizes the test helper instances, keeping one per test helper class.
|
9
|
+
# Test helpers are not mutable, they return a new instance every time an
|
10
|
+
# operation is performed, so it's safe to apply this optimization.
|
11
|
+
def get_test_helper(helper_name)
|
12
|
+
ivar_name = "@#{ helper_name }_capybara_test_helper"
|
13
|
+
instance_variable_get(ivar_name) ||
|
14
|
+
instance_variable_set(ivar_name, get_test_helper_class(helper_name).new(self))
|
15
|
+
end
|
16
|
+
|
17
|
+
# Internal: Requires a test helper file and memoizes the class for all tests.
|
18
|
+
#
|
19
|
+
# Returns a Class that subclasses BaseTestHelper.
|
20
|
+
def get_test_helper_class(name)
|
21
|
+
file_name = "#{ name }_test_helper"
|
22
|
+
ivar_name = "@#{ file_name }_test_helper_class"
|
23
|
+
instance_variable_get(ivar_name) || begin
|
24
|
+
require_test_helper(file_name)
|
25
|
+
test_helper_class = file_name.camelize.constantize
|
26
|
+
test_helper_class.on_test_helper_load
|
27
|
+
instance_variable_set(ivar_name, test_helper_class)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Internal: Requires a test helper file.
|
32
|
+
def require_test_helper(name)
|
33
|
+
CapybaraTestHelpers.config.helpers_paths.each do |path|
|
34
|
+
require Pathname.new(File.expand_path(path)).join("#{ name }.rb").to_s
|
35
|
+
return true # Don't check on the result, it could have been required earlier.
|
36
|
+
rescue LoadError
|
37
|
+
false
|
38
|
+
end
|
39
|
+
raise LoadError, "No '#{ name }.rb' file found in #{ CapybaraTestHelpers.config.helpers_paths.inspect }. "\
|
40
|
+
'Check for typos, or make sure the dirs in `CapybaraTestHelpers.config.helpers_paths` are in the load path.'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
Capybara.extend(CapybaraTestHelpers::DependencyInjection)
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Internal: Wraps Capybara finders to be aware of the selector aliases, and to
|
4
|
+
# auto-wrap the returned elements with test helpers.
|
5
|
+
module CapybaraTestHelpers::Finders
|
6
|
+
%i[
|
7
|
+
find
|
8
|
+
find_all
|
9
|
+
find_field
|
10
|
+
find_link
|
11
|
+
find_button
|
12
|
+
find_by_id
|
13
|
+
first
|
14
|
+
ancestor
|
15
|
+
sibling
|
16
|
+
].each do |method_name|
|
17
|
+
CapybaraTestHelpers.define_helper_method(self, method_name, wrap: true)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Public: Returns all the Capybara nodes that match the specified selector.
|
21
|
+
#
|
22
|
+
# Returns an Array of Capybara::Element that match the query.
|
23
|
+
def all(*args, **kwargs, &filter)
|
24
|
+
if defined?(::RSpec::Matchers::BuiltIn::All) && args.first.respond_to?(:matches?)
|
25
|
+
::RSpec::Matchers::BuiltIn::All.new(*args, **kwargs)
|
26
|
+
else
|
27
|
+
find_all(*args, **kwargs, &filter)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# Internal: Finds an element that matches the specified locator and options.
|
34
|
+
#
|
35
|
+
# Returns a Capybara::Node::Element that matches the conditions, or fails.
|
36
|
+
def find_element(*args, **kwargs, &filter)
|
37
|
+
kwargs[:test_helper] = self
|
38
|
+
current_context.find(*args, **kwargs, &filter)
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Internal: Wraps Capybara matchers to enable locator aliases, and to wrap the
|
4
|
+
# result with a test helper so that methods can be chained in a fluent style.
|
5
|
+
module CapybaraTestHelpers::Matchers
|
6
|
+
%i[
|
7
|
+
has_selector?
|
8
|
+
has_no_selector?
|
9
|
+
has_css?
|
10
|
+
has_no_css?
|
11
|
+
has_xpath?
|
12
|
+
has_no_xpath?
|
13
|
+
has_link?
|
14
|
+
has_no_link?
|
15
|
+
has_button?
|
16
|
+
has_no_button?
|
17
|
+
has_field?
|
18
|
+
has_no_field?
|
19
|
+
has_select?
|
20
|
+
has_no_select?
|
21
|
+
has_table?
|
22
|
+
has_no_table?
|
23
|
+
has_checked_field?
|
24
|
+
has_no_checked_field?
|
25
|
+
has_unchecked_field?
|
26
|
+
has_no_unchecked_field?
|
27
|
+
has_title?
|
28
|
+
has_no_title?
|
29
|
+
has_title?
|
30
|
+
has_no_title?
|
31
|
+
].each do |method_name|
|
32
|
+
CapybaraTestHelpers.define_helper_method(self, method_name)
|
33
|
+
end
|
34
|
+
|
35
|
+
%i[
|
36
|
+
has_ancestor?
|
37
|
+
has_no_ancestor?
|
38
|
+
has_sibling?
|
39
|
+
has_no_sibling?
|
40
|
+
matches_selector?
|
41
|
+
not_matches_selector?
|
42
|
+
matches_css?
|
43
|
+
not_matches_css?
|
44
|
+
matches_xpath?
|
45
|
+
not_matches_xpath?
|
46
|
+
has_text?
|
47
|
+
has_no_text?
|
48
|
+
has_content?
|
49
|
+
has_no_content?
|
50
|
+
matches_style?
|
51
|
+
has_style?
|
52
|
+
].each do |method_name|
|
53
|
+
CapybaraTestHelpers.define_helper_method(self, method_name, target: :to_capybara_node)
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'capybara_test_helpers'
|
4
|
+
require 'capybara/rspec'
|
5
|
+
|
6
|
+
# Public: Use in an RSpec describe block or in an included module to make
|
7
|
+
# helpers available on all specs.
|
8
|
+
def use_test_helpers(*names)
|
9
|
+
names.each do |name|
|
10
|
+
let(name) { get_test_helper(name) }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
RSpec.configure do |config|
|
15
|
+
# Make it available only in certain types of tests.
|
16
|
+
types = %i[feature system view]
|
17
|
+
|
18
|
+
# Options that will register a test helper for the test to use.
|
19
|
+
keys = %i[capybara_test_helpers test_helpers helpers]
|
20
|
+
|
21
|
+
# Inject test helpers by using a :helpers or :test_helpers option.
|
22
|
+
inject_test_helpers = proc { |example|
|
23
|
+
keys.flat_map { |key| example.metadata[key] }.compact.each do |name|
|
24
|
+
example.example_group_instance.define_singleton_method(name) { get_test_helper(name) }
|
25
|
+
end
|
26
|
+
}
|
27
|
+
|
28
|
+
# Allow injecting test helpers in a feature or scenario.
|
29
|
+
%i[feature system view].each do |type|
|
30
|
+
config.include(CapybaraTestHelpers::DependencyInjection, type: type)
|
31
|
+
config.before(:each, type: type, &inject_test_helpers)
|
32
|
+
end
|
33
|
+
end
|