capybara-compose 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +27 -0
- data/README.md +124 -0
- data/lib/capybara/compose.rb +74 -0
- data/lib/capybara/compose/actions.rb +116 -0
- data/lib/capybara/compose/assertions.rb +142 -0
- data/lib/capybara/compose/benchmark_helpers.rb +87 -0
- data/lib/capybara/compose/cucumber.rb +12 -0
- data/lib/capybara/compose/dependency_injection.rb +44 -0
- data/lib/capybara/compose/finders.rb +40 -0
- data/lib/capybara/compose/matchers.rb +79 -0
- data/lib/capybara/compose/rspec.rb +33 -0
- data/lib/capybara/compose/selectors.rb +178 -0
- data/lib/capybara/compose/synchronization.rb +41 -0
- data/lib/capybara/compose/test_helper.rb +171 -0
- data/lib/capybara/compose/to_or_expectation_handler.rb +23 -0
- data/lib/capybara/compose/version.rb +8 -0
- data/lib/generators/test_helper/test_helper_generator.rb +34 -0
- metadata +106 -0
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
require 'active_support/core_ext/numeric/time'
|
5
|
+
require 'rainbow'
|
6
|
+
|
7
|
+
# rubocop:disable Style/ClassVars
|
8
|
+
|
9
|
+
# Public: Keeps track of the running time for user-defined helpers, useful as a
|
10
|
+
# way to keep track of the executed methods, and to easily spot slow operations.
|
11
|
+
module Capybara::Compose::BenchmarkHelpers
|
12
|
+
extend ActiveSupport::Concern
|
13
|
+
|
14
|
+
included do
|
15
|
+
@@indentation_level = 0
|
16
|
+
@@indented_logs = []
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
# Internal: Helper to benchmark an operation, outputs the method name, its
|
22
|
+
# arguments, and the ellapsed time in milliseconds.
|
23
|
+
def benchmark_method(method_name, args, kwargs)
|
24
|
+
@@indented_logs.push(log = +'') # Push it in order, set the content later.
|
25
|
+
@@indentation_level += 1
|
26
|
+
before = Time.now
|
27
|
+
yield
|
28
|
+
ensure
|
29
|
+
diff_in_millis = (Time.now - before).in_milliseconds.round
|
30
|
+
@@indentation_level -= 1
|
31
|
+
|
32
|
+
# Set the queued message with the method call and the ellapsed time.
|
33
|
+
log.sub!('', _benchmark_str(method_name: method_name, args: args, kwargs: kwargs, time: diff_in_millis))
|
34
|
+
|
35
|
+
# Print the messages once we outdent all, and clear the queue.
|
36
|
+
@@indented_logs.each { |inner_log| Kernel.puts(inner_log) }.clear if @@indentation_level.zero?
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# Internal: Indents nested method calls, and adds color to make it readable.
|
42
|
+
def _benchmark_str(method_name:, args:, kwargs:, time:)
|
43
|
+
args += [kwargs] unless kwargs.empty?
|
44
|
+
args_str = args.map(&:inspect)
|
45
|
+
[
|
46
|
+
' ' * @@indentation_level,
|
47
|
+
Rainbow(self.class.name.chomp('TestHelper') + '#').slategray.rjust(40),
|
48
|
+
Rainbow(method_name.to_s).cyan,
|
49
|
+
Rainbow("(#{ args_str.join(', ') })").slategray,
|
50
|
+
' ',
|
51
|
+
Rainbow("#{ time } ms").send(time > 1000 && :red || time > 100 && :yellow || :green),
|
52
|
+
].join('')
|
53
|
+
end
|
54
|
+
|
55
|
+
module ClassMethods
|
56
|
+
# Hook: Benchmarks all methods in the class once it's loaded.
|
57
|
+
def on_test_helper_load
|
58
|
+
super
|
59
|
+
benchmark_all
|
60
|
+
end
|
61
|
+
|
62
|
+
# Debug: Wraps all instance methods of the test helper class to log them.
|
63
|
+
def benchmark_all
|
64
|
+
return if defined?(@benchmarked_all)
|
65
|
+
|
66
|
+
benchmark(instance_methods - superclass.instance_methods - [:lazy_for])
|
67
|
+
@benchmarked_all = true
|
68
|
+
end
|
69
|
+
|
70
|
+
# Debug: Wraps a method to output its parameters and ellapsed time.
|
71
|
+
#
|
72
|
+
# Usage:
|
73
|
+
# benchmark :input_for
|
74
|
+
# benchmark def input_for(...)
|
75
|
+
def benchmark(method_names)
|
76
|
+
prepend(Module.new {
|
77
|
+
Array.wrap(method_names).each do |method_name|
|
78
|
+
define_method(method_name) { |*args, **kwargs, &block|
|
79
|
+
benchmark_method(method_name, args, kwargs) { super(*args, **kwargs, &block) }
|
80
|
+
}
|
81
|
+
end
|
82
|
+
})
|
83
|
+
method_names
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
# rubocop:enable Style/ClassVars
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'capybara/compose'
|
4
|
+
|
5
|
+
World(Capybara::Compose::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 Capybara::Compose::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
|
+
Capybara::Compose.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 #{ Capybara::Compose.config.helpers_paths.inspect }. "\
|
40
|
+
'Check for typos, or make sure the dirs in `Capybara::Compose.config.helpers_paths` are in the load path.'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
Capybara.extend(Capybara::Compose::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 Capybara::Compose::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
|
+
Capybara::Compose.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,79 @@
|
|
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 Capybara::Compose::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
|
+
Capybara::Compose.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
|
+
Capybara::Compose.define_helper_method(self, method_name, target: :to_capybara_node)
|
54
|
+
end
|
55
|
+
|
56
|
+
%i[
|
57
|
+
assert_selector
|
58
|
+
assert_all_of_selectors
|
59
|
+
assert_none_of_selectors
|
60
|
+
assert_any_of_selectors
|
61
|
+
assert_no_selector
|
62
|
+
].each do |method_name|
|
63
|
+
Capybara::Compose.define_helper_method(self, method_name)
|
64
|
+
end
|
65
|
+
|
66
|
+
%i[
|
67
|
+
assert_matches_selector
|
68
|
+
assert_not_matches_selector
|
69
|
+
assert_ancestor
|
70
|
+
assert_no_ancestor
|
71
|
+
assert_sibling
|
72
|
+
assert_no_sibling
|
73
|
+
].each do |method_name|
|
74
|
+
Capybara::Compose.define_helper_method(self, method_name, target: :to_capybara_node)
|
75
|
+
end
|
76
|
+
|
77
|
+
alias has? has_selector?
|
78
|
+
alias has_no? has_no_selector?
|
79
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'capybara/compose'
|
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/compose 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
|
+
types.each do |type|
|
30
|
+
config.include(Capybara::Compose::DependencyInjection, type: type)
|
31
|
+
config.before(:each, type: type, &inject_test_helpers)
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/array/wrap'
|
4
|
+
|
5
|
+
# Internal: Avoid warnings in assert_valid_keys for passing the `test_helper` option.
|
6
|
+
Capybara::Queries::BaseQuery.prepend(Module.new {
|
7
|
+
attr_reader :test_helper
|
8
|
+
|
9
|
+
def initialize(options)
|
10
|
+
@test_helper = options.delete(:test_helper)
|
11
|
+
end
|
12
|
+
})
|
13
|
+
|
14
|
+
# Internal: Handle locator aliases provided in the test helper to finders,
|
15
|
+
# matchers, assertions, and actions.
|
16
|
+
Capybara::Queries::SelectorQuery.prepend(Module.new {
|
17
|
+
def initialize(*args, **options, &filter_block)
|
18
|
+
# Resolve any locator aliases defined in the test helper where this query
|
19
|
+
# originated from (through a finder, assertion, or matcher).
|
20
|
+
if test_helper = options[:test_helper]
|
21
|
+
args, options, filter_block = test_helper.resolve_alias_for_selector_query(args, options, filter_block)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Unwrap any test helpers that were provided to the :label selector, since
|
25
|
+
# it's making an explicit check by class.
|
26
|
+
options[:for] = options[:for].to_capybara_node if options[:for].is_a?(Capybara::Compose::TestHelper)
|
27
|
+
|
28
|
+
super(*args, **options, &filter_block)
|
29
|
+
end
|
30
|
+
})
|
31
|
+
|
32
|
+
# Public: Adds aliasing for element selectors to make it easier to encapsulate
|
33
|
+
# how to find a particular kind of element in the UI.
|
34
|
+
module Capybara::Compose::Selectors
|
35
|
+
SELECTOR_SEPARATOR = ','
|
36
|
+
|
37
|
+
def self.included(base)
|
38
|
+
base.extend(ClassMethods)
|
39
|
+
end
|
40
|
+
|
41
|
+
module ClassMethods
|
42
|
+
# Public: Light wrapper as syntax sugar for defining SELECTORS.
|
43
|
+
def aliases(selectors = {})
|
44
|
+
const_set('SELECTORS', selectors)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Public: Returns the available selectors for the test helper, or an empty
|
48
|
+
# Hash if selectors are not defined.
|
49
|
+
def selectors
|
50
|
+
unless defined?(@selectors)
|
51
|
+
parent_selectors = superclass.respond_to?(:selectors) ? superclass.selectors : {}
|
52
|
+
child_selectors = (defined?(self::SELECTORS) && self::SELECTORS || {})
|
53
|
+
.tap { |new_selectors| validate_selectors(new_selectors) }
|
54
|
+
@selectors = parent_selectors.merge(child_selectors).transform_values(&:freeze).freeze
|
55
|
+
end
|
56
|
+
@selectors
|
57
|
+
end
|
58
|
+
|
59
|
+
# Internal: Allows to "call" selectors, as a shortcut for find.
|
60
|
+
#
|
61
|
+
# Example: table.header == table.find(:header)
|
62
|
+
def define_getters_for_selectors
|
63
|
+
selectors.each_key do |selector_name|
|
64
|
+
define_method(selector_name) { |*args, **kwargs, &block|
|
65
|
+
find(selector_name, *args, **kwargs, &block)
|
66
|
+
}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Internal: Validates that all the selectors defined in the class won't
|
71
|
+
# cause confusion or misbehavior.
|
72
|
+
def validate_selectors(selectors)
|
73
|
+
selectors.each_key do |name|
|
74
|
+
if Capybara::Selector.all.key?(name)
|
75
|
+
raise "A selector with the name #{ name.inspect } is already registered in Capybara," \
|
76
|
+
" consider renaming the #{ name.inspect } alias in #{ self.class.name } to avoid confusion."
|
77
|
+
end
|
78
|
+
if Capybara::Compose::RESERVED_METHODS.include?(name)
|
79
|
+
raise "A method with the name #{ name.inspect } is part of the Capybara DSL," \
|
80
|
+
" consider renaming the #{ name.inspect } alias in #{ self.class.name } to avoid confusion."
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Public: Returns the available selectors for the test helper, or an empty
|
87
|
+
# Hash if selectors are not defined.
|
88
|
+
def selectors
|
89
|
+
self.class.selectors
|
90
|
+
end
|
91
|
+
|
92
|
+
# Internal: Inspects the arguments for a SelectorQuery, and resolves a
|
93
|
+
# selector alias if provided.
|
94
|
+
#
|
95
|
+
# Returns a pair of arguments and keywords to initialize a SelectorQuery.
|
96
|
+
def resolve_alias_for_selector_query(args, kwargs, filter_block)
|
97
|
+
# Extract the explicitly provided selector, if any. Example: `find_button`.
|
98
|
+
explicit_type = args.shift if selectors.key?(args[1])
|
99
|
+
|
100
|
+
if selectors.key?(args[0])
|
101
|
+
args, kwargs = locator_for(*args, **kwargs)
|
102
|
+
|
103
|
+
# Remove the type provided by the alias, and add back the explicit one.
|
104
|
+
if explicit_type
|
105
|
+
args.shift if args[0].is_a?(Symbol)
|
106
|
+
args.unshift(explicit_type)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
[args, kwargs, wrap_filter(filter_block)]
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
# Internal: Checks if there's a Capybara selector defined by that name.
|
116
|
+
def registered_selector?(name)
|
117
|
+
Capybara::Selector.all.key?(name)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Internal: Returns the query locator defined under the specified alias.
|
121
|
+
#
|
122
|
+
# NOTE: In most cases, the query locator is a simple CSS selector, but it also
|
123
|
+
# supports any of the built-in Capybara selectors.
|
124
|
+
#
|
125
|
+
# Returns an Array with the Capybara locator arguments, and options if any.
|
126
|
+
def locator_for(*args, **kwargs)
|
127
|
+
if args.size == 1
|
128
|
+
args = [*Array.wrap(resolve_locator_alias(args.first))]
|
129
|
+
kwargs = args.pop.deep_merge(kwargs) if args.last.is_a?(Hash)
|
130
|
+
end
|
131
|
+
[args, kwargs]
|
132
|
+
rescue KeyError => error
|
133
|
+
raise NotImplementedError, "A selector in #{ self.class.name } is not defined, #{ error.message }"
|
134
|
+
end
|
135
|
+
|
136
|
+
# Internal: Resolves one of the segments of a locator alias.
|
137
|
+
def resolve_locator_alias(fragment)
|
138
|
+
return fragment unless fragment.is_a?(Symbol) && (selectors.key?(fragment) || !registered_selector?(fragment))
|
139
|
+
|
140
|
+
locator = selectors.fetch(fragment)
|
141
|
+
|
142
|
+
locator.is_a?(Array) ? combine_locator_fragments(locator) : locator
|
143
|
+
end
|
144
|
+
|
145
|
+
# Internal: Resolves a complex locator alias, which might reference other
|
146
|
+
# locator aliases as well.
|
147
|
+
def combine_locator_fragments(fragments)
|
148
|
+
return fragments unless fragments.any? { |fragment| fragment.is_a?(Symbol) }
|
149
|
+
|
150
|
+
fragments = fragments.map { |fragment| resolve_locator_alias(fragment) }
|
151
|
+
flat_fragments = fragments.flatten(1)
|
152
|
+
type = flat_fragments.shift if flat_fragments.first.is_a?(Symbol)
|
153
|
+
|
154
|
+
# Only flatten fragments if it's CSS or XPath
|
155
|
+
if type.nil? || type == :css || type == :xpath
|
156
|
+
fragments = flat_fragments
|
157
|
+
else
|
158
|
+
type = nil
|
159
|
+
end
|
160
|
+
|
161
|
+
options = fragments.pop if fragments.last.is_a?(Hash)
|
162
|
+
|
163
|
+
[type, *combine_css_selectors(fragments), options].compact
|
164
|
+
end
|
165
|
+
|
166
|
+
# Internal: Combines parent and child classes to preserve the order.
|
167
|
+
def combine_css_selectors(selectors)
|
168
|
+
return selectors unless selectors.size > 1 && selectors.all? { |selector| selector.is_a?(String) }
|
169
|
+
|
170
|
+
selectors.reduce { |parent_selectors, children_selectors|
|
171
|
+
parent_selectors.split(SELECTOR_SEPARATOR).flat_map { |parent_selector|
|
172
|
+
children_selectors.split(SELECTOR_SEPARATOR).map { |children_selector|
|
173
|
+
"#{ parent_selector }#{ children_selector }"
|
174
|
+
}
|
175
|
+
}.join(SELECTOR_SEPARATOR)
|
176
|
+
}
|
177
|
+
end
|
178
|
+
end
|