capybara-compose 1.0.4

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