capybara-compose 1.0.4

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