capybara_test_helpers 1.0.0

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