capybara_test_helpers 1.0.0

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