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.
@@ -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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Easily write fluent Page Objects for Capybara in Ruby.
4
+ module Capybara
5
+ module Compose
6
+ VERSION = '1.0.4'
7
+ end
8
+ 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: []