capybara_test_helpers 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,174 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'capybara/rspec'
|
4
|
+
require 'active_support/core_ext/array/wrap'
|
5
|
+
|
6
|
+
# Internal: Avoid warnings in assert_valid_keys for passing the `test_helper` option.
|
7
|
+
Capybara::Queries::BaseQuery.prepend(Module.new {
|
8
|
+
attr_reader :test_helper
|
9
|
+
|
10
|
+
def initialize(options)
|
11
|
+
@test_helper = options.delete(:test_helper)
|
12
|
+
end
|
13
|
+
})
|
14
|
+
|
15
|
+
# Internal: Handle locator aliases provided in the test helper to finders,
|
16
|
+
# matchers, assertions, and actions.
|
17
|
+
Capybara::Queries::SelectorQuery.prepend(Module.new {
|
18
|
+
def initialize(*args, **options, &filter_block)
|
19
|
+
# Resolve any locator aliases defined in the test helper where this query
|
20
|
+
# originated from (through a finder, assertion, or matcher).
|
21
|
+
if test_helper = options[:test_helper]
|
22
|
+
args, options, filter_block = test_helper.resolve_alias_for_selector_query(args, options, filter_block)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Unwrap any test helpers that were provided to the :label selector, since
|
26
|
+
# it's making an explicit check by class.
|
27
|
+
options[:for] = options[:for].to_capybara_node if options[:for].is_a?(CapybaraTestHelpers::TestHelper)
|
28
|
+
|
29
|
+
super(*args, **options, &filter_block)
|
30
|
+
end
|
31
|
+
})
|
32
|
+
|
33
|
+
# Public: Adds aliasing for element selectors to make it easier to encapsulate
|
34
|
+
# how to find a particular kind of element in the UI.
|
35
|
+
module CapybaraTestHelpers::Selectors
|
36
|
+
SELECTOR_SEPARATOR = ','
|
37
|
+
|
38
|
+
def self.included(base)
|
39
|
+
base.extend(ClassMethods)
|
40
|
+
end
|
41
|
+
|
42
|
+
module ClassMethods
|
43
|
+
# Public: Returns the available selectors for the test helper, or an empty
|
44
|
+
# Hash if selectors are not defined.
|
45
|
+
def selectors
|
46
|
+
unless defined?(@selectors)
|
47
|
+
parent_selectors = superclass.respond_to?(:selectors) ? superclass.selectors : {}
|
48
|
+
child_selectors = (defined?(self::SELECTORS) && self::SELECTORS || {})
|
49
|
+
.tap { |new_selectors| validate_selectors(new_selectors) }
|
50
|
+
@selectors = parent_selectors.merge(child_selectors).transform_values(&:freeze).freeze
|
51
|
+
end
|
52
|
+
@selectors
|
53
|
+
end
|
54
|
+
|
55
|
+
# Internal: Allows to "call" selectors, as a shortcut for find.
|
56
|
+
#
|
57
|
+
# Example: table.header == table.find(:header)
|
58
|
+
def define_getters_for_selectors
|
59
|
+
selectors.each_key do |selector_name|
|
60
|
+
define_method(selector_name) { |*args, **kwargs, &block|
|
61
|
+
find(selector_name, *args, **kwargs, &block)
|
62
|
+
}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Internal: Validates that all the selectors defined in the class won't
|
67
|
+
# cause confusion or misbehavior.
|
68
|
+
def validate_selectors(selectors)
|
69
|
+
selectors.each_key do |name|
|
70
|
+
if Capybara::Selector.all.key?(name)
|
71
|
+
raise "A selector with the name #{ name.inspect } is already registered in Capybara," \
|
72
|
+
" consider renaming the #{ name.inspect } alias in #{ self.class.name } to avoid confusion."
|
73
|
+
end
|
74
|
+
if CapybaraTestHelpers::RESERVED_METHODS.include?(name)
|
75
|
+
raise "A method with the name #{ name.inspect } is part of the Capybara DSL," \
|
76
|
+
" consider renaming the #{ name.inspect } alias in #{ self.class.name } to avoid confusion."
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Public: Returns the available selectors for the test helper, or an empty
|
83
|
+
# Hash if selectors are not defined.
|
84
|
+
def selectors
|
85
|
+
self.class.selectors
|
86
|
+
end
|
87
|
+
|
88
|
+
# Internal: Inspects the arguments for a SelectorQuery, and resolves a
|
89
|
+
# selector alias if provided.
|
90
|
+
#
|
91
|
+
# Returns a pair of arguments and keywords to initialize a SelectorQuery.
|
92
|
+
def resolve_alias_for_selector_query(args, kwargs, filter_block)
|
93
|
+
# Extract the explicitly provided selector, if any. Example: `find_button`.
|
94
|
+
explicit_type = args.shift if selectors.key?(args[1])
|
95
|
+
|
96
|
+
if selectors.key?(args[0])
|
97
|
+
args, kwargs = locator_for(*args, **kwargs)
|
98
|
+
|
99
|
+
# Remove the type provided by the alias, and add back the explicit one.
|
100
|
+
if explicit_type
|
101
|
+
args.shift if args[0].is_a?(Symbol)
|
102
|
+
args.unshift(explicit_type)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
[args, kwargs, wrap_filter(filter_block)]
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
# Internal: Checks if there's a Capybara selector defined by that name.
|
112
|
+
def registered_selector?(name)
|
113
|
+
Capybara::Selector.all.key?(name)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Internal: Returns the query locator defined under the specified alias.
|
117
|
+
#
|
118
|
+
# NOTE: In most cases, the query locator is a simple CSS selector, but it also
|
119
|
+
# supports any of the built-in Capybara selectors.
|
120
|
+
#
|
121
|
+
# Returns an Array with the Capybara locator arguments, and options if any.
|
122
|
+
def locator_for(*args, **kwargs)
|
123
|
+
if args.size == 1
|
124
|
+
args = [*Array.wrap(resolve_locator_alias(args.first))]
|
125
|
+
kwargs = args.pop.deep_merge(kwargs) if args.last.is_a?(Hash)
|
126
|
+
end
|
127
|
+
[args, kwargs]
|
128
|
+
rescue KeyError => error
|
129
|
+
raise NotImplementedError, "A selector in #{ self.class.name } is not defined, #{ error.message }"
|
130
|
+
end
|
131
|
+
|
132
|
+
# Internal: Resolves one of the segments of a locator alias.
|
133
|
+
def resolve_locator_alias(fragment)
|
134
|
+
return fragment unless fragment.is_a?(Symbol) && (selectors.key?(fragment) || !registered_selector?(fragment))
|
135
|
+
|
136
|
+
locator = selectors.fetch(fragment)
|
137
|
+
|
138
|
+
locator.is_a?(Array) ? combine_locator_fragments(locator) : locator
|
139
|
+
end
|
140
|
+
|
141
|
+
# Internal: Resolves a complex locator alias, which might reference other
|
142
|
+
# locator aliases as well.
|
143
|
+
def combine_locator_fragments(fragments)
|
144
|
+
return fragments unless fragments.any? { |fragment| fragment.is_a?(Symbol) }
|
145
|
+
|
146
|
+
fragments = fragments.map { |fragment| resolve_locator_alias(fragment) }
|
147
|
+
flat_fragments = fragments.flatten(1)
|
148
|
+
type = flat_fragments.shift if flat_fragments.first.is_a?(Symbol)
|
149
|
+
|
150
|
+
# Only flatten fragments if it's CSS or XPath
|
151
|
+
if type.nil? || type == :css || type == :xpath
|
152
|
+
fragments = flat_fragments
|
153
|
+
else
|
154
|
+
type = nil
|
155
|
+
end
|
156
|
+
|
157
|
+
options = fragments.pop if fragments.last.is_a?(Hash)
|
158
|
+
|
159
|
+
[type, *combine_css_selectors(fragments), options].compact
|
160
|
+
end
|
161
|
+
|
162
|
+
# Internal: Combines parent and child classes to preserve the order.
|
163
|
+
def combine_css_selectors(selectors)
|
164
|
+
return selectors unless selectors.size > 1 && selectors.all? { |selector| selector.is_a?(String) }
|
165
|
+
|
166
|
+
selectors.reduce { |parent_selectors, children_selectors|
|
167
|
+
parent_selectors.split(SELECTOR_SEPARATOR).flat_map { |parent_selector|
|
168
|
+
children_selectors.split(SELECTOR_SEPARATOR).map { |children_selector|
|
169
|
+
"#{ parent_selector }#{ children_selector }"
|
170
|
+
}
|
171
|
+
}.join(SELECTOR_SEPARATOR)
|
172
|
+
}
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Internal: Provides helper functions to perform asynchronous assertions.
|
4
|
+
module CapybaraTestHelpers::Synchronization
|
5
|
+
# Internal: Necessary because the RSpec exception is not a StandardError and
|
6
|
+
# thus capybara does not rescue it, so it wouldn't attempt to retry it.
|
7
|
+
class ExpectationError < StandardError; end
|
8
|
+
|
9
|
+
# Internal: Errors that will be retried in a `synchronize_expectation` block.
|
10
|
+
EXPECTATION_ERRORS = [
|
11
|
+
ExpectationError,
|
12
|
+
Capybara::ElementNotFound,
|
13
|
+
(Selenium::WebDriver::Error::StaleElementReferenceError if defined?(Selenium::WebDriver::Error::StaleElementReferenceError)),
|
14
|
+
].compact.freeze
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
# Public: Can be used to make an asynchronous expectation, that will be
|
19
|
+
# retried until the max wait time configured in Capybara.
|
20
|
+
def synchronize_expectation(retry_on_errors: [], **options)
|
21
|
+
synchronize(errors: EXPECTATION_ERRORS + retry_on_errors, **options) {
|
22
|
+
begin
|
23
|
+
yield
|
24
|
+
rescue RSpec::Expectations::ExpectationNotMetError => error
|
25
|
+
# NOTE: Rethrow as ExpectationError because the RSpec exception is not
|
26
|
+
# a StandardError so capybara wouldn't rescue it inside synchronize.
|
27
|
+
raise ExpectationError, error
|
28
|
+
end
|
29
|
+
}
|
30
|
+
rescue ExpectationError => error
|
31
|
+
raise error.cause # Unwrap this internal error and raise the original error.
|
32
|
+
end
|
33
|
+
|
34
|
+
# Public: Used to implement more specific synchronization helpers.
|
35
|
+
#
|
36
|
+
# By default Capybara's methods like `find` and `have_css` already use
|
37
|
+
# synchronize to achieve asynchronicity, so it's not necessary to use this.
|
38
|
+
def synchronize(wait: Capybara.default_max_wait_time == 0 ? Capybara.default_max_wait_time : 3, **options, &block)
|
39
|
+
(current_element? ? current_context : page.document).synchronize(wait, **options, &block)
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'capybara/rspec'
|
4
|
+
|
5
|
+
require 'active_support/core_ext/module/delegation'
|
6
|
+
require 'active_support/core_ext/string/inflections'
|
7
|
+
require 'active_support/core_ext/object/blank'
|
8
|
+
|
9
|
+
require 'capybara_test_helpers/selectors'
|
10
|
+
require 'capybara_test_helpers/synchronization'
|
11
|
+
require 'capybara_test_helpers/finders'
|
12
|
+
require 'capybara_test_helpers/actions'
|
13
|
+
require 'capybara_test_helpers/assertions'
|
14
|
+
require 'capybara_test_helpers/matchers'
|
15
|
+
|
16
|
+
# Public: Base class to create test helpers that have full access to the
|
17
|
+
# Capybara DSL, while easily defining custom assertions, getters, and actions.
|
18
|
+
#
|
19
|
+
# It also supports locator aliases to prevent duplication and keep tests easier
|
20
|
+
# to understand and to maintain.
|
21
|
+
class CapybaraTestHelpers::TestHelper
|
22
|
+
include RSpec::Matchers
|
23
|
+
include RSpec::Mocks::ExampleMethods
|
24
|
+
include Capybara::DSL
|
25
|
+
include CapybaraTestHelpers::Selectors
|
26
|
+
include CapybaraTestHelpers::Synchronization
|
27
|
+
include CapybaraTestHelpers::Finders
|
28
|
+
include CapybaraTestHelpers::Actions
|
29
|
+
include CapybaraTestHelpers::Assertions
|
30
|
+
include CapybaraTestHelpers::Matchers
|
31
|
+
|
32
|
+
undef_method(*CapybaraTestHelpers::SKIPPED_DSL_METHODS)
|
33
|
+
|
34
|
+
attr_reader :query_context, :test_context
|
35
|
+
|
36
|
+
def initialize(query_context, test_context: query_context, negated: nil)
|
37
|
+
@query_context, @test_context, @negated = query_context, test_context, negated
|
38
|
+
end
|
39
|
+
|
40
|
+
# Public: To make the benchmark log less verbose.
|
41
|
+
def inspect
|
42
|
+
%(#<#{ self.class.name } #{ current_element? ? %(tag="#{ base.tag_name }") : object_id }>)
|
43
|
+
rescue *page.driver.invalid_element_errors
|
44
|
+
%(#<#{ self.class.name } #{ object_id }>)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Public: Makes it easier to inspect the current element.
|
48
|
+
def inspect_node
|
49
|
+
to_capybara_node.inspect
|
50
|
+
end
|
51
|
+
|
52
|
+
# Public: Casts the current context as a Capybara::Node::Element.
|
53
|
+
#
|
54
|
+
# NOTE: Uses the :el convention, which means actions can be performed directly
|
55
|
+
# on the test helper if an :el selector is defined.
|
56
|
+
def to_capybara_node
|
57
|
+
return current_context if current_element?
|
58
|
+
return find_element(:el) if selectors.key?(:el)
|
59
|
+
|
60
|
+
raise_missing_element_error
|
61
|
+
end
|
62
|
+
|
63
|
+
# Public: Wraps a Capybara::Node::Element with a test helper object.
|
64
|
+
def wrap_element(capybara_node)
|
65
|
+
if capybara_node.is_a?(Enumerable)
|
66
|
+
capybara_node.map { |node| wrap_element(node) }
|
67
|
+
else
|
68
|
+
self.class.new(capybara_node, test_context: test_context)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Public: Wraps a CapybaraTestHelper with a different test helper object.
|
73
|
+
def wrap_test_helper(test_helper)
|
74
|
+
self.class.new(test_helper.query_context, test_context: test_context) if test_helper.query_context
|
75
|
+
end
|
76
|
+
|
77
|
+
# Public: Scopes the Capybara queries in the block to be inside the specified
|
78
|
+
# selector.
|
79
|
+
def within_element(*args, **kwargs, &block)
|
80
|
+
locator = args.empty? ? [self] : args
|
81
|
+
kwargs[:test_helper] = self
|
82
|
+
page.within(*locator, **kwargs, &block)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Public: Scopes the Capybara queries in the block to be inside the specified
|
86
|
+
# selector.
|
87
|
+
def within(*args, **kwargs, &block)
|
88
|
+
return be_within(*args, **kwargs) unless block_given? # RSpec matcher.
|
89
|
+
|
90
|
+
within_element(*args, **kwargs, &block)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Public: Unscopes the inner block from any previous `within` calls.
|
94
|
+
def within_document
|
95
|
+
page.instance_exec { scopes << nil }
|
96
|
+
yield wrap_element(page.document)
|
97
|
+
ensure
|
98
|
+
page.instance_exec { scopes.pop }
|
99
|
+
end
|
100
|
+
|
101
|
+
# Internal: Returns the name of the class without the suffix.
|
102
|
+
#
|
103
|
+
# Example: 'current_page' for CurrentPageTestHelper.
|
104
|
+
def friendly_name
|
105
|
+
self.class.name.chomp('TestHelper').underscore
|
106
|
+
end
|
107
|
+
|
108
|
+
protected
|
109
|
+
|
110
|
+
# Internal: Used to perform assertions and others.
|
111
|
+
def current_context
|
112
|
+
query_context.respond_to?(:to_capybara_node) ? query_context : page
|
113
|
+
end
|
114
|
+
|
115
|
+
# Internal: Returns true if the current context is an element.
|
116
|
+
def current_element?
|
117
|
+
current_context.is_a?(Capybara::Node::Element)
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
# Internal: Wraps the optional filter block to ensure we pass it a test helper
|
123
|
+
# instead of a raw Capybara::Node::Element.
|
124
|
+
def wrap_filter(filter)
|
125
|
+
proc { |capybara_node_element| filter.call(wrap_element(capybara_node_element)) } if filter
|
126
|
+
end
|
127
|
+
|
128
|
+
# Internal: Helper to provide more information on the error.
|
129
|
+
def raise_missing_element_error
|
130
|
+
method_caller = caller.select { |x| x['test_helpers'] }[1]
|
131
|
+
method_caller_name = method_caller&.match(/in `(\w+)'/)
|
132
|
+
method_caller_name = method_caller_name ? method_caller_name[1] : method_caller
|
133
|
+
raise ArgumentError, "You are calling the `#{ method_caller_name }' method on the test helper but :el is not defined nor there's a current element.\n"\
|
134
|
+
'Define :el, or find an element before performing the action.'
|
135
|
+
end
|
136
|
+
|
137
|
+
class << self
|
138
|
+
# Public: Make methods in the test context available in the helpers.
|
139
|
+
def delegate_to_test_context(*method_names)
|
140
|
+
delegate(*method_names, to: :test_context)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Public: Allows to define dependencies on other matchers.
|
144
|
+
#
|
145
|
+
# NOTE: When you call a helper the "negated" state is preserved for assertions.
|
146
|
+
#
|
147
|
+
# NOTE: You can also pass an element to a test helper to "wrap" a specified
|
148
|
+
# element with the specified test helper class.
|
149
|
+
#
|
150
|
+
# Example:
|
151
|
+
# dropdown(element).toggle_menu
|
152
|
+
def use_test_helpers(*helper_names)
|
153
|
+
helper_names.each do |helper_name|
|
154
|
+
private define_method(helper_name) { |element = nil|
|
155
|
+
test_helper = test_context.get_test_helper(helper_name)
|
156
|
+
if element
|
157
|
+
raise ArgumentError, "#{ element.inspect } must be a test helper or element." unless element.respond_to?(:to_capybara_node)
|
158
|
+
|
159
|
+
test_helper = test_helper.wrap_element(element.to_capybara_node)
|
160
|
+
end
|
161
|
+
@negated.nil? ? test_helper : test_helper.should(@negated)
|
162
|
+
}
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# Internal: Allows to perform certain actions just before a test helper will
|
167
|
+
# be loaded for the first time.
|
168
|
+
def on_test_helper_load
|
169
|
+
define_getters_for_selectors
|
170
|
+
end
|
171
|
+
|
172
|
+
# Internal: Fail early if a reserved method is redefined.
|
173
|
+
def method_added(method_name)
|
174
|
+
return unless CapybaraTestHelpers::RESERVED_METHODS.include?(method_name)
|
175
|
+
|
176
|
+
raise "A method with the name #{ method_name.inspect } is part of the Capybara DSL," \
|
177
|
+
' overriding it could cause unexpected issues that could be very hard to debug.'
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
Capybara::TestHelper = CapybaraTestHelpers::TestHelper unless defined?(Capybara::TestHelper)
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'capybara/rspec'
|
4
|
+
|
5
|
+
# Internal: Used heavily in the RSpec matchers, makes it very easy to create
|
6
|
+
# a dual assertion (can be used as positive or negative).
|
7
|
+
#
|
8
|
+
# See https://maximomussini.com/posts/cucumber-to_or_not_to/
|
9
|
+
module CapybaraTestHelpers::ToOrExpectationHandler
|
10
|
+
# Public: Allows a more convenient definition of should/should not Gherkin steps.
|
11
|
+
#
|
12
|
+
# Example:
|
13
|
+
#
|
14
|
+
# Then(/^I should (not )?see "(.*)"$/) do |not_to, text|
|
15
|
+
# expect(page).to_or not_to, have_content(text)
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
def to_or(not_to, matcher, message = nil, &block)
|
19
|
+
if not_to
|
20
|
+
not_to(matcher, message, &block)
|
21
|
+
else
|
22
|
+
to(matcher, message, &block)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
RSpec::Expectations::ExpectationTarget.include(CapybaraTestHelpers::ToOrExpectationHandler)
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'capybara_test_helpers'
|
2
|
+
require 'rails/generators/named_base'
|
3
|
+
|
4
|
+
# Internal: Generates a new test helper file in the appropriate directory.
|
5
|
+
class TestHelperGenerator < Rails::Generators::NamedBase
|
6
|
+
def create_helper_file
|
7
|
+
create_file("#{ CapybaraTestHelpers.config.helpers_paths.first }/#{ file_name }_test_helper.rb") {
|
8
|
+
<<~CAPYBARA_TEST_HELPER
|
9
|
+
# frozen_string_literal: true
|
10
|
+
|
11
|
+
class #{ file_name.camelize }TestHelper < #{ file_name.to_s == 'base' ? 'Capybara::TestHelper' : 'BaseTestHelper' }
|
12
|
+
# Selectors: Semantic aliases for elements, a useful abstraction.
|
13
|
+
SELECTORS = {}
|
14
|
+
|
15
|
+
# Getters: A convenient way to get related data or nested elements.
|
16
|
+
|
17
|
+
# Actions: Encapsulate complex actions to provide a cleaner interface.
|
18
|
+
|
19
|
+
# Assertions: Check on element properties, used with `should` and `should_not`.
|
20
|
+
|
21
|
+
# Background: Helpers to add/modify/delete data in the database or session.
|
22
|
+
end
|
23
|
+
CAPYBARA_TEST_HELPER
|
24
|
+
}
|
25
|
+
end
|
26
|
+
end
|