capybara-compose 1.0.4

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