capybara_test_helpers 1.0.0

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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f491ad2f4d94ecd7e159466955e97b5e87117a5bb3fdb8eb907139e25fcb7ef1
4
+ data.tar.gz: 0f2d79cf1b201ffc6f4a2b7601b2550c85e626f2ebe32f367538e49a5adfcde2
5
+ SHA512:
6
+ metadata.gz: c323a983bc4b02cde81006a986388c8cb1a5c88c4db8a8c4b9a2bc746765c8b6b46487593b25ce2a46628865b6b2bb54cbb6c1afeeaea7965667778e60044e2b
7
+ data.tar.gz: 7b77a14b74c33d4a934071014a43db8ed63c184fea3e5ef8fc91a1ed4439f7e3a40398b87cd763249771b6a813e3a608fcd1caff93c941d4baa97db60f711b7e
@@ -0,0 +1,3 @@
1
+ ## Capybara Test Helpers 1.0.0 (2020-11-17) ##
2
+
3
+ * Initial Release.
@@ -0,0 +1,401 @@
1
+ <h1 align="center">
2
+ Capybara Test Helpers
3
+ <p align="center">
4
+ <a href="https://github.com/ElMassimo/capybara_test_helpers/actions"><img alt="Build Status" src="https://github.com/ElMassimo/capybara_test_helpers/workflows/build/badge.svg"/></a>
5
+ <a href="https://inch-ci.org/github/ElMassimo/capybara_test_helpers"><img alt="Inline docs" src="https://inch-ci.org/github/ElMassimo/capybara_test_helpers.svg"/></a>
6
+ <a href="https://codeclimate.com/github/ElMassimo/capybara_test_helpers"><img alt="Maintainability" src="https://codeclimate.com/github/ElMassimo/capybara_test_helpers/badges/gpa.svg"/></a>
7
+ <a href="https://codeclimate.com/github/ElMassimo/capybara_test_helpers"><img alt="Test Coverage" src="https://codeclimate.com/github/ElMassimo/capybara_test_helpers/badges/coverage.svg"/></a>
8
+ <a href="https://rubygems.org/gems/capybara_test_helpers"><img alt="Gem Version" src="https://img.shields.io/gem/v/capybara_test_helpers.svg?colorB=e9573f"/></a>
9
+ <a href="https://github.com/ElMassimo/capybara_test_helpers/blob/master/LICENSE.txt"><img alt="License" src="https://img.shields.io/badge/license-MIT-428F7E.svg"/></a>
10
+ </p>
11
+ </h1>
12
+
13
+ [__Capybara Test Helpers__](https://github.com/ElMassimo/capybara_test_helpers) is
14
+ an opinionated library built on top of [capybara], that encourages good testing
15
+ practices based on encapsulation and reuse.
16
+
17
+ Write tests that everyone can understand, and leverage your Ruby skills to keep tests __easy to read and easy to change__.
18
+
19
+ [capybara]: https://github.com/teamcapybara/capybara
20
+ [capybara dsl]: https://github.com/teamcapybara/capybara#the-dsl
21
+ [capybara querying]: https://github.com/teamcapybara/capybara#querying
22
+ [cucumber]: https://github.com/cucumber/cucumber-ruby
23
+ [rspec]: https://github.com/rspec/rspec
24
+ [rspec matchers]: https://relishapp.com/rspec/rspec-expectations/docs/built-in-matchers
25
+ [rspec-rails]: https://github.com/rspec/rspec-rails#installation
26
+ [trailing_commas]: https://maximomussini.com/posts/trailing-commas/
27
+ [testing_robots]: https://jakewharton.com/testing-robots/
28
+ [page_objects]: https://martinfowler.com/bliki/PageObject.html
29
+ [rspec_injection]: https://github.com/ElMassimo/capybara_test_helpers/blob/master/examples/rails_app/spec/system/cities_spec.rb#L7
30
+ [rspec_global_injection]: https://github.com/ElMassimo/capybara_test_helpers/blob/master/examples/rails_app/spec/support/default_test_helpers.rb#L8
31
+ [cucumber_injection]: https://github.com/ElMassimo/capybara_test_helpers/blob/master/examples/rails_app/features/step_definitions/city_steps.rb#L3
32
+ [example app]: https://github.com/ElMassimo/capybara_test_helpers/blob/master/examples/rails_app
33
+ [capybara_test_helpers_tests]: https://github.com/ElMassimo/capybara_test_helpers/blob/master/spec
34
+ [positive and negative assertions]: https://maximomussini.com/posts/cucumber-to_or_not_to/
35
+ [should]: https://github.com/ElMassimo/capybara_test_helpers/blob/master/lib/capybara_test_helpers/assertions.rb#L10-L15
36
+ [should_not]: https://github.com/ElMassimo/capybara_test_helpers/blob/master/lib/capybara_test_helpers/assertions.rb#L17-L22
37
+ [rails_integration]: https://github.com/ElMassimo/capybara_test_helpers/commit/c512e39987215e30227dad45e775480bc1348325
38
+ [cucumber_integration]: https://github.com/ElMassimo/capybara_test_helpers/commit/68e20cb40ba409c50f88f8b745eb908fb067a0aa
39
+
40
+ ## Why? 🤔
41
+
42
+ [`capybara`][capybara] is a great library for integration tests in Ruby,
43
+ commonly used in combination with [RSpec] or [cucumber].
44
+
45
+ Although [cucumber] encourages good practices such as writing steps at a high
46
+ level, thinking in terms of the user rather than the interactions required, it
47
+ __doesn't scale well__ in a large project. Steps are available for all tests,
48
+ and there's no way to partition or isolate them.
49
+
50
+ At the same time, Gherkin is very limited as a language, it can be very awkward
51
+ to use when steps require parameters, and it's hard to find and detect duplicate
52
+ steps, and very __time consuming__ to refactor them.
53
+
54
+ In contrast, writing tests in [RSpec] has a very low barrier since Ruby is a joy
55
+ to work with, but you are on your own to encapsulate code to avoid coupling
56
+ tests to the current UI. Small changes to the UI should not require rewriting
57
+ dozens of tests, but __without clear guidelines__ it's hard to achieve good tests.
58
+
59
+ This library provides __a solid foundation__ of simple and repeatable patterns
60
+ that can be used to write better tests.
61
+
62
+ ## Features ⚡️
63
+
64
+ - Leverage your __Ruby__ skills for keeping tests in good shape
65
+ - Powerful syntax for __assertions__ (without monkey patching)
66
+ - __Aliases__ for element locators to avoid repetition
67
+ - __Composability__: define interactions with your UI once, and [focus on the tests][testing robots] many times
68
+ - Dependency injection to make tests __predictable and robust__
69
+ - Full access to the __[Capybara DSL]__
70
+
71
+ ## Installation 💿
72
+
73
+ Add this line to your application's Gemfile:
74
+
75
+ ```ruby
76
+ gem 'capybara_test_helpers'
77
+ ```
78
+
79
+ And then run:
80
+
81
+ $ bundle install
82
+
83
+ ### RSpec
84
+
85
+ To use with [RSpec], require the following in `spec_helper.rb`:
86
+
87
+ ```ruby
88
+ require 'capybara_test_helpers/rspec'
89
+ ```
90
+
91
+ #### In Rails
92
+
93
+ If using Rails, make sure you [follow the setup in `rspec-rails`][rspec-rails] first.
94
+
95
+ You can run `rails g test_helper base` to create a base test helper and require
96
+ it as well so that other test helpers can extend it without manually requiring.
97
+
98
+ ```ruby
99
+ # spec/rails_helper.rb
100
+ require 'capybara_test_helpers/rspec'
101
+ require Rails.root.join('test_helpers/base_test_helper')
102
+ ```
103
+
104
+ [Check this example][rails_integration] to see how you can get started.
105
+
106
+ ### Cucumber
107
+
108
+ To use with [Cucumber], require the following in `env.rb`:
109
+
110
+ ```ruby
111
+ require 'capybara_test_helpers/cucumber'
112
+ require Rails.root.join('test_helpers/base_test_helper')
113
+ ```
114
+
115
+ Have in mind that RSpec is a much better fit, as Gherkin is very limited.
116
+
117
+ That said, test helpers do provide [a nice way to share code](https://github.com/ElMassimo/capybara_test_helpers/blob/master/examples/rails_app/features/step_definitions/city_steps.rb) if you are migrating
118
+ from Cucumber to RSpec.
119
+
120
+ [Check this example][cucumber_integration] to see how you can get started.
121
+
122
+ ## Usage 🚀
123
+
124
+ You can define a test helper by subclassing `Capybara::TestHelper`, which has
125
+ full access to the Capybara DSL.
126
+
127
+ When using Rails, you can generate a test helper by running:
128
+
129
+ rails g test_helper users
130
+
131
+ ```ruby
132
+ class UsersTestHelper < Capybara::TestHelper
133
+ # Selectors: Semantic aliases for elements, a useful abstraction.
134
+ SELECTORS = {
135
+ el: 'table.users',
136
+ form: '.user-form',
137
+ submit_button: [:button, type: 'submit'],
138
+ }
139
+
140
+ # Getters: A convenient way to get related data or nested elements.
141
+ def row_for(user)
142
+ within { find(:table_row, { 'Name' => user.name }) }
143
+ end
144
+
145
+ # Actions: Encapsulate complex actions to provide a cleaner interface.
146
+ def add(attrs)
147
+ click_on('Add User')
148
+ save_user(**attrs)
149
+ end
150
+
151
+ def edit(user, with:)
152
+ row_for(user).click_on('Edit')
153
+ save_user(**with)
154
+ end
155
+
156
+ def delete(user)
157
+ row_for(user).click_on('Delete')
158
+ accept_confirm
159
+ end
160
+
161
+ private \
162
+ def save_user(name:, language:)
163
+ within(:form) {
164
+ fill_in('Name', with: name)
165
+ choose('Language', option: language)
166
+ submit_button.click
167
+ }
168
+ end
169
+
170
+ # Assertions: Allow to check on element properties while keeping it DRY.
171
+ def have_user(name:, language:)
172
+ columns = { 'Name' => name, 'Language' => language }
173
+ within { have(:table_row, columns) }
174
+ end
175
+ end
176
+ ```
177
+
178
+ To make the test helper available you can [use the `test_helpers` option in RSpec][rspec_injection],
179
+ or [call `use_test_helpers` in Cucumber step definitions][cucumber_injection].
180
+
181
+ For test helpers that you expect to use very often, [`use_test_helpers` allows you to make them available globally][rspec_global_injection].
182
+
183
+ ### Writing a Test with Helpers ✅
184
+
185
+ You can find [this working example](https://github.com/ElMassimo/capybara_test_helpers/blob/master/examples/rails_app/spec/features/cities_spec.rb) and more in the [example app] and the [Capybara tests][capybara_test_helpers_tests].
186
+
187
+ ```ruby
188
+ require 'rails_helper'
189
+
190
+ RSpec.describe 'Cities', test_helpers: [:cities] do
191
+ let!(:nyc) { cities.given_there_is_a_city('NYC') }
192
+
193
+ before { cities.visit_page }
194
+
195
+ scenario 'valid inputs' do
196
+ cities.add(name: 'Minneapolis')
197
+ cities.should.have_city('Minneapolis')
198
+ end
199
+
200
+ scenario 'invalid inputs' do
201
+ cities.add(name: '') { |form|
202
+ form.should.have_error("Name can't be blank")
203
+ }
204
+ end
205
+
206
+ scenario 'editing a city' do
207
+ cities.edit(nyc, with: { name: 'New York City' })
208
+ cities.should_no_longer.have_city('NYC')
209
+ cities.should_now.have_city('New York City')
210
+ end
211
+
212
+ scenario 'deleting a city', screen_size: :phone do
213
+ cities.delete(nyc)
214
+ cities.should_no_longer.have_city('NYC')
215
+ end
216
+ end
217
+ ```
218
+
219
+ ## DSL 🛠
220
+
221
+ A documentation website with the full API and examples is coming soon.
222
+
223
+ Every single method in the [Capybara DSL] is available inside test helpers, as
224
+ well as the [built-in RSpec matchers][rspec matchers].
225
+
226
+ ### Selectors 🔍
227
+
228
+ You can encapsulate locators for commonly used elements to avoid hardcoding them
229
+ in different tests.
230
+
231
+ As a result, if the implementation changes, there are less places that need to
232
+ be updated, making it faster to update tests after UI changes.
233
+
234
+ ```ruby
235
+ class FormTestHelper < BaseTestHelper
236
+ SELECTORS = {
237
+ el: '.form',
238
+ error_summary: ['#error_explanation', visible: true],
239
+ name_input: [:fillable_field, 'Name'],
240
+ save_button: [:button, type: 'submit'],
241
+ }
242
+ ```
243
+
244
+ You can then leverage these aliases on any Capybara method:
245
+
246
+ ```ruby
247
+ # Finding an element
248
+ form.find(:save_button, visible: false)
249
+
250
+ # Interacting with an element
251
+ form.fill_in(:name_input, with: 'Jane')
252
+
253
+ # Making an assertion
254
+ form.has_selector?(:error_summary, text: "Can't be blank")
255
+ ```
256
+
257
+ #### Syntax Sugar
258
+
259
+ To avoid repetition, getters are available for every selector alias:
260
+
261
+ ```ruby
262
+ form.find(:name_input)
263
+ # same as
264
+ form.name_input
265
+
266
+ form.find(:error_summary, text: "Can't be blank")
267
+ # same as
268
+ form.error_summary(text: "Can't be blank")
269
+ ```
270
+
271
+ #### `:el` convention
272
+
273
+ By convention, `:el` is the top-level element of the component or page the test
274
+ helper is encapsulating, which will be used automatically when calling a
275
+ Capybara operation that requires a node, such as `click` or `value`.
276
+
277
+ ```ruby
278
+ form.within { save_button.click }
279
+ # same as
280
+ form.within(:el) { save_button.click }
281
+ # same as
282
+ form.el.within { save_button.click }
283
+ ```
284
+
285
+ ### Assertions ☑️
286
+
287
+ You can use any of the [RSpec matchers provided by Capybara][capybara querying],
288
+ but the way to use them in test helpers is slightly different.
289
+
290
+ Before using an assertion, you must call [`should`][should] or [`should_not`][should_not], and then
291
+ chain the RSpec matcher or your own custom assertion.
292
+
293
+ ```ruby
294
+ users.find(:table)
295
+ .should.have_selector(:table_row, ['Jane', 'Doe']
296
+ .should_not.have_selector(:table_row, ['John', 'Doe'])
297
+ ```
298
+
299
+ #### Custom Assertions 🎩
300
+
301
+ The example above becomes a lot nicer if we define a more semantic assertion,
302
+ which can be easily done by leveraging an existing assertion:
303
+
304
+ ```ruby
305
+ class UsersTestHelper < BaseTestHelper
306
+ SELECTORS = {
307
+ list: 'table.users',
308
+ }
309
+
310
+ # Assertions: Check on element properties, used with `should` and `should_not`.
311
+ def have_user(*names)
312
+ have(:table_row, names)
313
+ end
314
+ ```
315
+
316
+ and then use it as:
317
+
318
+ ```ruby
319
+ users.list
320
+ .should.have_user('Jane', 'Doe')
321
+ .should_not.have_user('John', 'Doe')
322
+ ```
323
+
324
+ Notice that you don't need to define both the [positive and negative assertions],
325
+ they are both available because we are using an existing assertion.
326
+
327
+ #### Advanced Assertions ⚙️
328
+
329
+ Sometimes built-in assertions are not enough, and you need to use an expectation
330
+ directly. Test helpers provide a `to_or` and `not_to` method, similar to [the
331
+ technique described in this post][positive and negative assertions] that you
332
+ can use to implement an assertion that you can use with `should` or `should_not`.
333
+
334
+ ```ruby
335
+ # frozen_string_literal: true
336
+
337
+ class CurrentPageTestHelper < BaseTestHelper
338
+ # Getters: A convenient way to get related data or nested elements.
339
+ def fullscreen?
340
+ evaluate_script('!!(document.mozFullScreenElement || document.webkitFullscreenElement)')
341
+ end
342
+
343
+ # Assertions: Allow to check on element properties while keeping it DRY.
344
+ def be_fullscreen
345
+ expect(fullscreen?).to_or not_to, eq(true)
346
+ end
347
+ end
348
+
349
+ current_page.should.be_fullscreen
350
+ current_page.should_not.be_fullscreen
351
+ ```
352
+
353
+ You can make the assertion retry automatically until the Capybara timeout by
354
+ using `synchronize_expectation`:
355
+
356
+ ```ruby
357
+ def be_fullscreen
358
+ synchronize_expectation {
359
+ expect(fullscreen?).to_or not_to, eq(true)
360
+ }
361
+ end
362
+ ```
363
+
364
+ ## Design 📐
365
+
366
+ This library is loosely based on the concepts of [Page Objects][page_objects] and [Testing Robots][testing_robots], with a healthy dose of [dependency injection](https://martinfowler.com/articles/injection.html).
367
+
368
+ Capybara has a great DSL, so the focus of this library is to build upon it, by
369
+ allowing you to create your own actions and assertions and call them just as
370
+ fluidly as you would call `find` or `has_content?`.
371
+
372
+ This library works best when encapsulating common UI patterns in separate helpers,
373
+ such as a `FormTestHelper` or a `DropdownTestHelper`, and then reusing them in
374
+ page-specific test helpers to make the test read more semantically.
375
+
376
+ ## Formatting 📏
377
+
378
+ Regarding selectors, I highly recommend writing one attribute per line, sorting
379
+ them alphabetically (most editors can do it for you), and
380
+ [always using a trailing comma][trailing_commas].
381
+
382
+ ```ruby
383
+ class DropdownTestHelper < BaseTestHelper
384
+ # Selectors: Semantic aliases for elements, a useful abstraction.
385
+ SELECTORS = {
386
+ el: '.dropdown',
387
+ toggle: '.dropdown-toggle',
388
+ }
389
+ ```
390
+
391
+ It will minimize the amount of git conflicts, and keep the history a lot cleaner and more meaningful when using `git blame`.
392
+
393
+ ## Special Thanks 🙏
394
+
395
+ This library wouldn't be the same without the early validation from my colleagues, and numerous improvements and bugfixes they contributed to it. Thanks for the support 😃
396
+
397
+ - [capybara]: Solid library to write integration tests in Ruby.
398
+
399
+ ## License
400
+
401
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara_test_helpers/version'
4
+ require 'capybara_test_helpers/config'
5
+ require 'capybara_test_helpers/test_helper'
6
+ require 'capybara_test_helpers/dependency_injection'
@@ -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?(CapybaraTestHelpers::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?(CapybaraTestHelpers::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 CapybaraTestHelpers::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
+ CapybaraTestHelpers.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
+ CapybaraTestHelpers.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
+ CapybaraTestHelpers.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