capybara-compose 1.0.4
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.
- 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,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Internal: Provides helper functions to perform asynchronous assertions.
|
4
|
+
module Capybara::Compose::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, **options, &block)
|
39
|
+
(current_element? ? current_context : page.document).synchronize(wait, **options, &block)
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# NOTE: Optional dependencies.
|
4
|
+
require 'capybara/rspec' rescue nil
|
5
|
+
require 'capybara/minitest' rescue nil
|
6
|
+
|
7
|
+
require 'active_support/core_ext/module/delegation'
|
8
|
+
require 'active_support/core_ext/string/inflections'
|
9
|
+
require 'active_support/core_ext/object/blank'
|
10
|
+
|
11
|
+
# Public: Base class to create test helpers that have full access to the
|
12
|
+
# Capybara DSL, while easily defining custom assertions, getters, and actions.
|
13
|
+
#
|
14
|
+
# It also supports locator aliases to prevent duplication and keep tests easier
|
15
|
+
# to understand and to maintain.
|
16
|
+
class Capybara::Compose::TestHelper
|
17
|
+
include RSpec::Matchers rescue nil
|
18
|
+
include RSpec::Mocks::ExampleMethods rescue nil
|
19
|
+
include Capybara::Minitest::Assertions rescue nil
|
20
|
+
|
21
|
+
include Capybara::DSL
|
22
|
+
include Capybara::Compose::Selectors
|
23
|
+
include Capybara::Compose::Synchronization
|
24
|
+
include Capybara::Compose::Finders
|
25
|
+
include Capybara::Compose::Actions
|
26
|
+
include Capybara::Compose::Assertions
|
27
|
+
include Capybara::Compose::Matchers
|
28
|
+
|
29
|
+
undef_method(*Capybara::Compose::SKIPPED_DSL_METHODS)
|
30
|
+
|
31
|
+
attr_reader :query_context, :test_context
|
32
|
+
|
33
|
+
def initialize(query_context, test_context: query_context, negated: nil)
|
34
|
+
@query_context, @test_context, @negated = query_context, test_context, negated
|
35
|
+
end
|
36
|
+
|
37
|
+
# Public: To make the benchmark log less verbose.
|
38
|
+
def inspect
|
39
|
+
%(#<#{ self.class.name } #{ current_element? ? %(tag="#{ base.tag_name }") : object_id }>)
|
40
|
+
rescue *page.driver.invalid_element_errors
|
41
|
+
%(#<#{ self.class.name } #{ object_id }>)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Public: Makes it easier to inspect the current element.
|
45
|
+
def inspect_node
|
46
|
+
to_capybara_node.inspect
|
47
|
+
end
|
48
|
+
|
49
|
+
# Public: Casts the current context as a Capybara::Node::Element.
|
50
|
+
#
|
51
|
+
# NOTE: Uses the :el convention, which means actions can be performed directly
|
52
|
+
# on the test helper if an :el selector is defined.
|
53
|
+
def to_capybara_node
|
54
|
+
return current_context if current_element?
|
55
|
+
return find_element(:el) if selectors.key?(:el)
|
56
|
+
|
57
|
+
raise_missing_element_error
|
58
|
+
end
|
59
|
+
|
60
|
+
# Public: Wraps a Capybara::Node::Element or test helper with a test helper
|
61
|
+
# object of this class.
|
62
|
+
def wrap_element(element)
|
63
|
+
if element.is_a?(Enumerable)
|
64
|
+
element.map { |node| wrap_element(node) }
|
65
|
+
else
|
66
|
+
raise ArgumentError, "#{ element.inspect } must be a test helper or element." unless element.respond_to?(:to_capybara_node)
|
67
|
+
|
68
|
+
self.class.new(element.to_capybara_node, test_context: test_context)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Public: Scopes the Capybara queries in the block to be inside the specified
|
73
|
+
# selector.
|
74
|
+
def within_element(*args, **kwargs, &block)
|
75
|
+
locator = args.empty? ? [self] : args
|
76
|
+
kwargs[:test_helper] = self
|
77
|
+
page.within(*locator, **kwargs, &block)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Public: Scopes the Capybara queries in the block to be inside the specified
|
81
|
+
# selector.
|
82
|
+
def within(*args, **kwargs, &block)
|
83
|
+
return be_within(*args, **kwargs) unless block_given? # RSpec matcher.
|
84
|
+
|
85
|
+
within_element(*args, **kwargs, &block)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Public: Unscopes the inner block from any previous `within` calls.
|
89
|
+
def within_document
|
90
|
+
page.instance_exec { scopes << nil }
|
91
|
+
yield wrap_element(page.document)
|
92
|
+
ensure
|
93
|
+
page.instance_exec { scopes.pop }
|
94
|
+
end
|
95
|
+
|
96
|
+
# Internal: Returns the name of the class without the suffix.
|
97
|
+
#
|
98
|
+
# Example: 'current_page' for CurrentPageTestHelper.
|
99
|
+
def friendly_name
|
100
|
+
self.class.name.chomp('TestHelper').underscore
|
101
|
+
end
|
102
|
+
|
103
|
+
protected
|
104
|
+
|
105
|
+
# Internal: Used to perform assertions and others.
|
106
|
+
def current_context
|
107
|
+
query_context.respond_to?(:to_capybara_node) ? query_context : page
|
108
|
+
end
|
109
|
+
|
110
|
+
# Internal: Returns true if the current context is an element.
|
111
|
+
def current_element?
|
112
|
+
current_context.is_a?(Capybara::Node::Element)
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
# Internal: Wraps the optional filter block to ensure we pass it a test helper
|
118
|
+
# instead of a raw Capybara::Node::Element.
|
119
|
+
def wrap_filter(filter)
|
120
|
+
proc { |capybara_node_element| filter.call(wrap_element(capybara_node_element)) } if filter
|
121
|
+
end
|
122
|
+
|
123
|
+
# Internal: Helper to provide more information on the error.
|
124
|
+
def raise_missing_element_error
|
125
|
+
method_caller = caller.select { |x| x['test_helpers'] }[1]
|
126
|
+
method_caller_name = method_caller&.match(/in `(\w+)'/)
|
127
|
+
method_caller_name = method_caller_name ? method_caller_name[1] : method_caller
|
128
|
+
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"\
|
129
|
+
'Define :el, or find an element before performing the action.'
|
130
|
+
end
|
131
|
+
|
132
|
+
class << self
|
133
|
+
# Public: Make methods in the test context available in the helpers.
|
134
|
+
def delegate_to_test_context(*method_names)
|
135
|
+
delegate(*method_names, to: :test_context)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Public: Allows to make other test helpers available.
|
139
|
+
#
|
140
|
+
# NOTE: When you call a helper the "negated" state is preserved for assertions.
|
141
|
+
#
|
142
|
+
# NOTE: You can also pass an element to a test helper to "wrap" a specified
|
143
|
+
# element with the specified test helper class.
|
144
|
+
#
|
145
|
+
# Example:
|
146
|
+
# dropdown(element).toggle_menu
|
147
|
+
def use_test_helpers(*helper_names)
|
148
|
+
helper_names.each do |helper_name|
|
149
|
+
private define_method(helper_name) { |element = nil|
|
150
|
+
test_helper = test_context.get_test_helper(helper_name)
|
151
|
+
test_helper = test_helper.wrap_element(element) if element
|
152
|
+
@negated.nil? ? test_helper : test_helper.should(@negated)
|
153
|
+
}
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Internal: Allows to perform certain actions just before a test helper will
|
158
|
+
# be loaded for the first time.
|
159
|
+
def on_test_helper_load
|
160
|
+
define_getters_for_selectors
|
161
|
+
end
|
162
|
+
|
163
|
+
# Internal: Fail early if a reserved method is redefined.
|
164
|
+
def method_added(method_name)
|
165
|
+
return unless Capybara::Compose::RESERVED_METHODS.include?(method_name)
|
166
|
+
|
167
|
+
raise "A method with the name #{ method_name.inspect } is part of the Capybara DSL," \
|
168
|
+
' overriding it could cause unexpected issues that could be very hard to debug.'
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Internal: Used heavily in the RSpec matchers, makes it very easy to create
|
4
|
+
# a dual assertion (can be used as positive or negative).
|
5
|
+
#
|
6
|
+
# See https://maximomussini.com/posts/cucumber-to_or_not_to/
|
7
|
+
module Capybara::Compose::ToOrExpectationHandler
|
8
|
+
# Public: Allows a more convenient definition of should/should not Gherkin steps.
|
9
|
+
#
|
10
|
+
# Example:
|
11
|
+
#
|
12
|
+
# Then(/^I should (not )?see "(.*)"$/) do |not_to, text|
|
13
|
+
# expect(page).to_or not_to, have_content(text)
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
def to_or(not_to, matcher, message = nil, &block)
|
17
|
+
if not_to
|
18
|
+
not_to(matcher, message, &block)
|
19
|
+
else
|
20
|
+
to(matcher, message, &block)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'capybara/compose'
|
4
|
+
require 'rails/generators/named_base'
|
5
|
+
|
6
|
+
# Internal: Generates a new test helper file in the appropriate directory.
|
7
|
+
class TestHelperGenerator < Rails::Generators::NamedBase
|
8
|
+
def base_helper?
|
9
|
+
file_name.to_s == 'base'
|
10
|
+
end
|
11
|
+
|
12
|
+
def create_helper_file
|
13
|
+
create_file("#{ Capybara::Compose.config.helpers_paths.first }/#{ file_name }_test_helper.rb") {
|
14
|
+
<<~CAPYBARA_TEST_HELPER
|
15
|
+
# frozen_string_literal: true
|
16
|
+
|
17
|
+
class #{ file_name.camelize }TestHelper < #{ base_helper? ? 'Capybara::TestHelper' : 'BaseTestHelper' }
|
18
|
+
# Aliases: Semantic aliases for locators, can be used in most DSL methods.
|
19
|
+
aliases(
|
20
|
+
#{ base_helper? ? '# Avoid defining :el here since it will be inherited by all helpers.' : "# el: '.#{ file_name.tr('_', '-') }'," }
|
21
|
+
)
|
22
|
+
|
23
|
+
# Finders: A convenient way to get related data or nested elements.
|
24
|
+
|
25
|
+
# Actions: Encapsulate complex actions to provide a cleaner interface.
|
26
|
+
|
27
|
+
# Assertions: Check on element properties, used with `should` and `should_not`.
|
28
|
+
|
29
|
+
# Background: Helpers to add/modify/delete data in the database or session.
|
30
|
+
end
|
31
|
+
CAPYBARA_TEST_HELPER
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: capybara-compose
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.4
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Maximo Mussini
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-02-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: capybara
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: zeitwerk
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: Write tests that everyone can understand, and leverage your Ruby skills
|
56
|
+
to keep them easy to read and easy to change.
|
57
|
+
email:
|
58
|
+
- maximomussini@gmail.com
|
59
|
+
executables: []
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- CHANGELOG.md
|
64
|
+
- README.md
|
65
|
+
- lib/capybara/compose.rb
|
66
|
+
- lib/capybara/compose/actions.rb
|
67
|
+
- lib/capybara/compose/assertions.rb
|
68
|
+
- lib/capybara/compose/benchmark_helpers.rb
|
69
|
+
- lib/capybara/compose/cucumber.rb
|
70
|
+
- lib/capybara/compose/dependency_injection.rb
|
71
|
+
- lib/capybara/compose/finders.rb
|
72
|
+
- lib/capybara/compose/matchers.rb
|
73
|
+
- lib/capybara/compose/rspec.rb
|
74
|
+
- lib/capybara/compose/selectors.rb
|
75
|
+
- lib/capybara/compose/synchronization.rb
|
76
|
+
- lib/capybara/compose/test_helper.rb
|
77
|
+
- lib/capybara/compose/to_or_expectation_handler.rb
|
78
|
+
- lib/capybara/compose/version.rb
|
79
|
+
- lib/generators/test_helper/test_helper_generator.rb
|
80
|
+
homepage: https://github.com/ElMassimo/capybara-compose
|
81
|
+
licenses:
|
82
|
+
- MIT
|
83
|
+
metadata:
|
84
|
+
homepage_uri: https://github.com/ElMassimo/capybara-compose
|
85
|
+
source_code_uri: https://github.com/ElMassimo/capybara-compose
|
86
|
+
changelog_uri: https://github.com/ElMassimo/capybara-compose/blob/master/CHANGELOG.md
|
87
|
+
post_install_message:
|
88
|
+
rdoc_options: []
|
89
|
+
require_paths:
|
90
|
+
- lib
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: 2.3.0
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
requirements: []
|
102
|
+
rubygems_version: 3.2.3
|
103
|
+
signing_key:
|
104
|
+
specification_version: 4
|
105
|
+
summary: Easily write fluent Page Objects for Capybara in Ruby.
|
106
|
+
test_files: []
|