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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 037e1213be976eef937bb06defb927fc534915273d3f0b01388c78a0f9f69021
4
+ data.tar.gz: 64cf4560fc3a53a2333c02addb42336d768dbb7d115a41d18a26b584a0205884
5
+ SHA512:
6
+ metadata.gz: d35e1c57511578085cf4b7484b933d31dbbc16238b5c931d6592e389408d3b4b0d4ceb27bfba61682c2860dc6a9f22b662dacc928993c82a65d0445e0f2b2888
7
+ data.tar.gz: 9a48802d63b7b580f76ccc37e458587de08d6ae3374c21a46f3f650fa238815cce1bc0f79f9d0d9073735919dda9ea3cb6a093f926d0bb7ccfd6740ce999763e
data/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ ## Capybara Compose 2.0.0 (2021-02-24) ##
2
+
3
+ * Rebrand _Capybara Test Helpers_ as _Capybara Compose_, and emphasize how the advantage is in reusing modules shared by the community.
4
+
5
+ ## Capybara Test Helpers 1.0.4 (2020-11-26) ##
6
+
7
+ * Add `have_no` alias for `have_no_selector`.
8
+ * Add `has_no?` alias for `has_no_selector?`.
9
+ * Remove internal method `wrap_test_helper`.
10
+ * Allow to use locator aliases in `assert_` methods as well, for consistency.
11
+
12
+ ## Capybara Test Helpers 1.0.3 (2020-11-24) ##
13
+
14
+ * Add `Capybara::Compose::BenchmarkHelpers`
15
+
16
+ ## Capybara Test Helpers 1.0.2 (2020-11-23) ##
17
+
18
+ * Add `aliases` DSL to define `SELECTORS`.
19
+
20
+ ## Capybara Test Helpers 1.0.1 (2020-11-19) ##
21
+
22
+ * Add `has?` alias for `has_selector?`.
23
+ * Delegate `have` to the RSpec collection matcher when passing an Integer.
24
+
25
+ ## Capybara Test Helpers 1.0.0 (2020-11-17) ##
26
+
27
+ * Initial Release.
data/README.md ADDED
@@ -0,0 +1,124 @@
1
+ [docs]: https://capybara-test-helpers.netlify.app/
2
+ [guide]: https://capybara-test-helpers.netlify.app/guide/
3
+ [api]: https://capybara-test-helpers.netlify.app/api/
4
+ [design patterns]: https://capybara-test-helpers.netlify.app/guide/advanced/design-patterns
5
+ [installation]: https://capybara-test-helpers.netlify.app/installation
6
+ [capybara]: https://github.com/teamcapybara/capybara
7
+ [cucumber]: https://github.com/cucumber/cucumber-ruby
8
+ [current context]: https://capybara-test-helpers.netlify.app/guide/essentials/current-context
9
+ [rspec]: https://github.com/rspec/rspec
10
+ [aliases]: https://capybara-test-helpers.netlify.app/guide/essentials/aliases
11
+ [assertions]: https://capybara-test-helpers.netlify.app/guide/essentials/assertions
12
+ [synchronization]: https://capybara-test-helpers.netlify.app/guide/advanced/synchronization
13
+
14
+ <h1 align="center">
15
+ Capybara Test Helpers
16
+ <p align="center">
17
+ <a href="https://github.com/ElMassimo/capybara-compose/actions">
18
+ <img alt="Build Status" src="https://github.com/ElMassimo/capybara-compose/workflows/build/badge.svg"/>
19
+ </a>
20
+ <a href="https://codeclimate.com/github/ElMassimo/capybara-compose">
21
+ <img alt="Maintainability" src="https://codeclimate.com/github/ElMassimo/capybara-compose/badges/gpa.svg"/>
22
+ </a>
23
+ <a href="https://codeclimate.com/github/ElMassimo/capybara-compose">
24
+ <img alt="Test Coverage" src="https://codeclimate.com/github/ElMassimo/capybara-compose/badges/coverage.svg"/>
25
+ </a>
26
+ <a href="https://rubygems.org/gems/capybara-compose">
27
+ <img alt="Gem Version" src="https://img.shields.io/gem/v/capybara-compose.svg?colorB=e9573f"/>
28
+ </a>
29
+ <a href="https://github.com/ElMassimo/capybara-compose/blob/main/LICENSE.txt">
30
+ <img alt="License" src="https://img.shields.io/badge/license-MIT-428F7E.svg"/>
31
+ </a>
32
+ </p>
33
+ </h1>
34
+
35
+ [__Capybara Test Helpers__](https://github.com/ElMassimo/capybara-compose) allows you to easily encapsulate logic in your integration tests.
36
+
37
+ Write tests that everyone can understand, and leverage your Ruby skills to keep them __easy to read and easy to change__.
38
+
39
+ ## Features ⚡️
40
+
41
+ [Locator Aliases][aliases] work with every [Capybara] method, allowing you to encapsulate CSS selectors and labels, and avoid coupling tests with the implementation.
42
+
43
+ The [entire Capybara DSL is available][api], and element results are [wrapped automatically][current context] so that you can chain your own assertions and actions fluently.
44
+
45
+ A [powerful syntax for assertions][assertions] and convenient primitives for [synchronization] enable you to write async-aware expectations: say goodbye to flaky tests.
46
+
47
+ ## Documentation 📖
48
+
49
+ Visit the [documentation website][docs] to check out the [guides][guide], searchable [__API reference__][api], and examples.
50
+
51
+ ## Installation 💿
52
+
53
+ Add this line to your application's Gemfile:
54
+
55
+ ```ruby
56
+ gem 'capybara/compose'
57
+ ```
58
+
59
+ To use with [RSpec], add the following to your `spec_helper.rb`:
60
+
61
+ ```ruby
62
+ require 'capybara/compose/rspec'
63
+ ```
64
+
65
+ To use with [Cucumber], add the following to your `support/env.rb`:
66
+
67
+ ```ruby
68
+ require 'capybara/compose/cucumber'
69
+ ```
70
+
71
+ Additional installation instructions are available in the [documentation website][installation].
72
+
73
+ ## Quick Tour 🛣
74
+
75
+ Let's say we have a list of cities, and we want to test the _Edit_ functionality using [Capybara].
76
+
77
+ ```ruby
78
+ scenario 'editing a city' do
79
+ visit('/cities')
80
+
81
+ within('.cities') {
82
+ find(:table_row, { 'Name' => 'NYC' }).click_on('Edit')
83
+ }
84
+ fill_in 'Name', with: 'New York City'
85
+ click_on('Update City')
86
+
87
+ within('.cities') {
88
+ expect(page).not_to have_selector(:table_row, { 'Name' => 'NYC' })
89
+ expect(page).to have_selector(:table_row, { 'Name' => 'New York City' })
90
+ }
91
+ end
92
+ ```
93
+
94
+ Even though it gets the job done, it takes a while to understand what the test is trying to do.
95
+
96
+ Without discipline these tests can become __hard to manage__ and require __frequent updating__.
97
+
98
+ ### Using Test Helpers
99
+
100
+ We can avoid the duplication and keep the [focus on the test][design patterns] instead of its
101
+ implementation by using [__test helpers__][docs].
102
+
103
+ ```ruby
104
+ scenario 'editing a city', test_helpers: [:cities] do
105
+ cities.visit_page
106
+
107
+ cities.edit('NYC', with: { name: 'New York City' })
108
+
109
+ cities.should_no_longer.have_city('NYC')
110
+ cities.should_now.have_city('New York City')
111
+ end
112
+ ```
113
+
114
+ Learn more about it in the [documentation website][docs].
115
+
116
+ ## Special Thanks 🙏
117
+
118
+ This library wouldn't be the same without early validation from my colleagues, and numerous improvements and bugfixes they contributed to it. Thanks for the support 😃
119
+
120
+ - [capybara]: Excellent library to write integration tests in Ruby.
121
+
122
+ ## License
123
+
124
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara'
4
+
5
+ require 'zeitwerk'
6
+ loader = Zeitwerk::Loader.for_gem
7
+ loader.tag = 'capybara-compose'
8
+ loader.ignore(
9
+ File.expand_path("#{ __dir__ }/compose/rspec.rb"),
10
+ File.expand_path("#{ __dir__ }/compose/cucumber.rb"),
11
+ File.expand_path("#{ __dir__ }/compose/minitest.rb"),
12
+ )
13
+ loader.push_dir(__dir__, namespace: Capybara)
14
+ loader.setup
15
+
16
+ # Internal: Global configuration for the library.
17
+ module Capybara::Compose
18
+ DEFAULT_CONFIGURATION = {
19
+ helpers_paths: ['test_helpers'].freeze,
20
+ }.freeze
21
+
22
+ # Internal: Reserved methods for Capybara::TestHelper.
23
+ test_helper_methods = [
24
+ :page,
25
+ :find_element,
26
+ :should,
27
+ :should_not,
28
+ :not_to,
29
+ ].freeze
30
+
31
+ # Internal: Methods that are in the Capybara DSL but are so common that we
32
+ # don't want to issue a warning if they are used as selectors.
33
+ SKIPPED_DSL_METHODS = [
34
+ :title,
35
+ :body,
36
+ :html,
37
+ ].freeze
38
+
39
+ # Internal: Methods that should not be overiden or used as locator aliases to
40
+ # avoid confusion while working on test helpers.
41
+ RESERVED_METHODS = (Capybara::Session::DSL_METHODS - SKIPPED_DSL_METHODS + test_helper_methods).to_set.freeze
42
+
43
+ # Internal: Ruby 2.7 swallows keyword arguments, so for methods that take a
44
+ # Hash as the first argument as well as keyword arguments, we need to manually
45
+ # detect and move them to args if empty.
46
+ METHODS_EXPECTING_A_HASH = %i[matches_style? has_style? match_style have_style].to_set.freeze
47
+
48
+ # Internal: Struct for configuration.
49
+ Config = Struct.new(*DEFAULT_CONFIGURATION.keys)
50
+
51
+ # Public: Returns the current configuration for the test helpers.
52
+ def self.config
53
+ @config ||= Config.new(*DEFAULT_CONFIGURATION.values)
54
+ yield @config if block_given?
55
+ @config
56
+ end
57
+
58
+ # Internal: Allows to define methods that are a part of the Capybara DSL, as
59
+ # well as RSpec matchers.
60
+ def self.define_helper_method(klass, method_name, wrap: false, assertion: false, target: 'current_context', return_self: assertion, inject_test_helper: true)
61
+ klass.class_eval <<~HELPER, __FILE__, __LINE__ + 1
62
+ def #{ method_name }(*args, **kwargs, &filter)
63
+ #{ 'args.push(kwargs) && (kwargs = {}) if args.empty?' if METHODS_EXPECTING_A_HASH.include?(method_name) }
64
+ #{ 'kwargs[:test_helper] = self' if inject_test_helper }
65
+ #{ 'wrap_element ' if wrap }#{ assertion ? "expect(#{ target }).to_or not_to, test_context" : target }.#{ method_name }(*args, **kwargs, &filter)
66
+ #{ 'self' if return_self }
67
+ end
68
+ HELPER
69
+ end
70
+ end
71
+
72
+ # NOTE: Simplify migration from Capybara Test Helpers.
73
+ CapybaraTestHelpers = Capybara::Compose unless defined?(CapybaraTestHelpers)
74
+ Capybara::TestHelper = Capybara::Compose::TestHelper unless defined?(Capybara::TestHelper)
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Internal: Allows to pass test helpers as arguments to `evaluate_script`.
4
+ Capybara::Session.prepend(Module.new {
5
+ private
6
+
7
+ # Override: To ensure test helpers are sent to the driver as native elements.
8
+ def driver_args(args)
9
+ super(args.map { |arg| arg.is_a?(Capybara::Compose::TestHelper) ? arg.to_capybara_node : arg })
10
+ end
11
+ })
12
+
13
+ # Internal: Allows to pass a test helper to `scroll_to`.
14
+ Capybara::Node::Element.prepend(Module.new {
15
+ # Override: Unwrap capybara test helpers into a node.
16
+ def scroll_to(pos_or_x_or_el, *args, **kwargs)
17
+ pos_or_x_or_el = pos_or_x_or_el.to_capybara_node if pos_or_x_or_el.is_a?(Capybara::Compose::TestHelper)
18
+ super
19
+ end
20
+ })
21
+
22
+ # Internal: Wraps Capybara actions to enable locator aliases, and to wrap the
23
+ # result with a test helper so that methods can be chained in a fluent style.
24
+ module Capybara::Compose::Actions
25
+ delegate(
26
+ :==, # Allows to make comparisons more transparent.
27
+ :===, # Allows to ensure case statements inside Capybara work as expected.
28
+ to: :to_capybara_node,
29
+ )
30
+
31
+ delegate(
32
+ :[],
33
+ :all_text,
34
+ :checked?,
35
+ :disabled?,
36
+ :multiple?,
37
+ :obscured?,
38
+ :readonly?,
39
+ :selected?,
40
+ :base,
41
+ :native,
42
+ :path,
43
+ :rect,
44
+ :style,
45
+ :tag_name,
46
+ :text,
47
+ :value,
48
+ :visible?,
49
+ :visible_text,
50
+ to: :to_capybara_node,
51
+ )
52
+
53
+ delegate(
54
+ :evaluate_script,
55
+ :evaluate_async_script,
56
+ to: :current_context,
57
+ )
58
+
59
+ %i[
60
+ execute_script
61
+ scroll_to
62
+ ].each do |method_name|
63
+ Capybara::Compose.define_helper_method(self, method_name, return_self: true, inject_test_helper: false)
64
+ end
65
+
66
+ %i[
67
+ click
68
+ double_click
69
+ drag_to
70
+ drop
71
+ flash
72
+ hover
73
+ reload
74
+ right_click
75
+ select_option
76
+ send_keys
77
+ set
78
+ trigger
79
+ unselect_option
80
+ ].each do |method_name|
81
+ Capybara::Compose.define_helper_method(self, method_name, target: :to_capybara_node, return_self: true, inject_test_helper: false)
82
+ end
83
+
84
+ %i[
85
+ click_on
86
+ click_link_or_button
87
+ click_button
88
+ click_link
89
+ choose
90
+ check
91
+ uncheck
92
+ fill_in
93
+ attach_file
94
+ select
95
+ unselect
96
+ ].each do |method_name|
97
+ Capybara::Compose.define_helper_method(self, method_name, wrap: true)
98
+ end
99
+
100
+ # Public: Sets the value for the input, or presses the specified keys, one at a time.
101
+ def type_in(*text, typing: text.size > 1 || text.first.is_a?(Symbol) || text.first.is_a?(Array), **options)
102
+ typing ? send_keys(*text) : set(text.first, **options)
103
+ end
104
+
105
+ # Public: Useful to natively give focus to an element.
106
+ def focus
107
+ to_capybara_node.execute_script('this.focus()')
108
+ self
109
+ end
110
+
111
+ # Public: Useful to natively blur an element.
112
+ def blur
113
+ to_capybara_node.execute_script('this.blur()')
114
+ self
115
+ end
116
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ # NOTE: Optional dependency to enable `to_or`.
4
+ begin
5
+ require 'rspec/expectations'
6
+ RSpec::Expectations::ExpectationTarget.include(Capybara::Compose::ToOrExpectationHandler)
7
+ rescue LoadError
8
+ end
9
+
10
+ # Internal: Wraps RSpec matchers to allow using them after calling `should` or
11
+ # `should_not` to perform the assertion.
12
+ module Capybara::Compose::Assertions
13
+ # Public: Returns a test helper with a positive assertion state.
14
+ # Any assertions called after it will execute as `expect(...).to ...`.
15
+ def should(negated = false)
16
+ negated = !!negated # Coerce to boolean.
17
+ return self if negated == @negated
18
+
19
+ clone.tap { |test_helper| test_helper.instance_variable_set('@negated', negated) }
20
+ end
21
+ [:should_still, :should_now, :and, :and_instead, :and_also, :and_still].each { |should_alias| alias_method should_alias, :should }
22
+
23
+ # Public: Returns a test helper with a negative assertion state.
24
+ # Any assertions called after it will execute as `expect(...).not_to ...`.
25
+ def should_not
26
+ @negated ? self : should(true)
27
+ end
28
+ [:should_still_not, :should_no_longer, :nor, :and_not].each { |should_alias| alias_method should_alias, :should_not }
29
+
30
+ # Public: Makes it more readable when in used in combination with to_or.
31
+ def not_to
32
+ raise(ArgumentError, 'You must call `should` or `should_not` before calling this method') if @negated.nil?
33
+
34
+ @negated
35
+ end
36
+ alias or_should_not not_to
37
+
38
+ # Public: Allows to write complex nested assertions.
39
+ def invert_expectation
40
+ should(!not_to)
41
+ end
42
+
43
+ %i[
44
+ have_selector
45
+ have_no_selector
46
+ have_css
47
+ have_no_css
48
+ have_xpath
49
+ have_no_xpath
50
+ have_link
51
+ have_no_link
52
+ have_button
53
+ have_no_button
54
+ have_field
55
+ have_no_field
56
+ have_select
57
+ have_no_select
58
+ have_table
59
+ have_no_table
60
+ have_checked_field
61
+ have_no_checked_field
62
+ have_unchecked_field
63
+ have_no_unchecked_field
64
+ have_all_of_selectors
65
+ have_none_of_selectors
66
+ have_any_of_selectors
67
+ have_title
68
+ have_no_title
69
+ ].each do |method_name|
70
+ Capybara::Compose.define_helper_method(self, method_name, assertion: true)
71
+ end
72
+
73
+ %i[
74
+ have_ancestor
75
+ have_no_ancestor
76
+ have_sibling
77
+ have_no_sibling
78
+ match_selector
79
+ not_match_selector
80
+ match_css
81
+ not_match_css
82
+ match_xpath
83
+ not_match_xpath
84
+ have_text
85
+ have_no_text
86
+ have_content
87
+ have_no_content
88
+ match_style
89
+ have_style
90
+ ].each do |method_name|
91
+ Capybara::Compose.define_helper_method(self, method_name, target: :to_capybara_node, assertion: true)
92
+ end
93
+
94
+ %i[
95
+ have_current_path
96
+ have_no_current_path
97
+ ].each do |method_name|
98
+ Capybara::Compose.define_helper_method(self, method_name, target: :page, assertion: true, inject_test_helper: false)
99
+ end
100
+
101
+ # Public: Allows to call have_selector with a shorter syntax.
102
+ def have(*args, **kwargs, &filter)
103
+ if args.first.is_a?(Integer)
104
+ ::RSpec::CollectionMatchers::Have.new(*args, **kwargs)
105
+ else
106
+ have_selector(*args, **kwargs, &filter)
107
+ end
108
+ end
109
+
110
+ alias have_no have_no_selector
111
+
112
+ # Public: Allows to check on any input value asynchronously.
113
+ def have_value(expected_value, **options)
114
+ synchronize_expectation(**options) { expect(value).to_or not_to, eq(expected_value) }
115
+ self
116
+ end
117
+
118
+ private
119
+
120
+ BE_PREDICATE_REGEX = /^(?:be_(?:an?_)?)(.*)/
121
+ HAS_REGEX = /^(?:have_)(.*)/
122
+ DYNAMIC_MATCHER_REGEX = Regexp.union(BE_PREDICATE_REGEX, HAS_REGEX)
123
+
124
+ # Internal: Override the method_missing defined in RSpec::Matchers to avoid
125
+ # accidentally calling a predicate or has matcher instead of an assertion.
126
+ def method_missing(method, *args, **kwargs, &block)
127
+ case method.to_s
128
+ when DYNAMIC_MATCHER_REGEX
129
+ raise NoMethodError, "undefined method `#{ method }' for #{ inspect }.\nUse `delegate_to_test_context(:#{ method })` in the test helper class if you plan to use it often, or `test_context.#{ method }` as needed in the instance."
130
+ else
131
+ super
132
+ end
133
+ end
134
+
135
+ # Internal: Override the method_missing defined in RSpec::Matchers to avoid
136
+ # accidentally calling a predicate or has matcher instead of an assertion.
137
+ def respond_to_missing?(method, *)
138
+ return false if method =~ DYNAMIC_MATCHER_REGEX
139
+
140
+ super
141
+ end
142
+ end