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.
@@ -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