capybara_test_helpers 1.0.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 +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
|