capybara-compose 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|