capybara-lockstep 2.1.0 → 2.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 514789431b5b017176575acd43c1a5b6558040f31cf7cb4ac119cf10e756dbee
4
- data.tar.gz: 2945ac3b32b7f83a7bc2bd14848f67950c12e5063033d8dbe2063788dfe8c024
3
+ metadata.gz: 8bad1a61632d37c5e705b886be4cb840a62923f73887c1443060fb00c6e69aa2
4
+ data.tar.gz: 9243b559659a33a00e6f803304284aa9cd47e5c2f110a11b47eb2e4e62441537
5
5
  SHA512:
6
- metadata.gz: b2dac0508c3caaa13fd28fe285c9c3b7d2b796dfba6c2bf788578ffee90135b6504a2e9b731d8bf4496806723d26c28bf5ccb536e5de21c2b5661b354a9e2de1
7
- data.tar.gz: 4e80b685b588dfded10cb0783cbe62aceb00517199ef5527ce76fb76a234c96eb913ec0e7d08df2e22a7dd5a2b2d475bed0d4318add6ddee9fd27403afc2e0fe
6
+ metadata.gz: 9b285d685609f913c92b4b8d5556d34d7d77e2fb6d80e5fac4cda189c81a2b81445fc65a5680726c2d21b16d0bcdcb1fcb35e3a6169b2febca3244eaa555b985
7
+ data.tar.gz: d598437ffb3eaf066d6cbcb747ac6c5445bed0a2c2cbc5113f42e0d947d4b71fc7bb4752ede4193cca989acafd5d126757c67d3454fbfc226014d7b42056b55f
@@ -3,13 +3,13 @@ name: Tests
3
3
  on:
4
4
  push:
5
5
  branches:
6
- - master
6
+ - main
7
7
  pull_request:
8
8
  branches:
9
- - master
9
+ - main
10
10
  workflow_dispatch:
11
11
  branches:
12
- - master
12
+ - main
13
13
  jobs:
14
14
  test:
15
15
  runs-on: ubuntu-20.04
data/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file.
2
2
 
3
3
  This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
4
4
 
5
+ # 2.2.1
6
+
7
+ - Fixed a bug that disabled most functionality for drivers with `browser: :remote`.
8
+
9
+
10
+ # 2.2.0
11
+
12
+ - We now wait for `<video>` and `<audio>` elements to load their metadata. This addresses a race condition where a media element is inserted into the DOM, but another user action deletes or renames the source before the browser could load the initial metadata frames.
13
+ - We now wait for `<script type="module">`.
14
+ - We no longer wait for `<img loading="lazy">` or `<iframe loading="lazy">`. This prevents a deadlock where we would wait forever for an element that defers loading until it is scrolled into the viewport.
15
+
5
16
 
6
17
  # 2.1.0
7
18
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- capybara-lockstep (2.1.0)
4
+ capybara-lockstep (2.2.1)
5
5
  activesupport (>= 4.2)
6
6
  capybara (>= 3.0)
7
7
  ruby2_keywords
@@ -31,7 +31,7 @@ GEM
31
31
  childprocess (4.1.0)
32
32
  concurrent-ruby (1.1.10)
33
33
  daemons (1.4.1)
34
- diff-lcs (1.3)
34
+ diff-lcs (1.5.1)
35
35
  eventmachine (1.2.7)
36
36
  gemika (0.8.1)
37
37
  i18n (1.12.0)
@@ -58,21 +58,21 @@ GEM
58
58
  rake (13.1.0)
59
59
  regexp_parser (2.8.2)
60
60
  rexml (3.2.5)
61
- rspec (3.7.0)
62
- rspec-core (~> 3.7.0)
63
- rspec-expectations (~> 3.7.0)
64
- rspec-mocks (~> 3.7.0)
65
- rspec-core (3.7.0)
66
- rspec-support (~> 3.7.0)
67
- rspec-expectations (3.7.0)
61
+ rspec (3.13.0)
62
+ rspec-core (~> 3.13.0)
63
+ rspec-expectations (~> 3.13.0)
64
+ rspec-mocks (~> 3.13.0)
65
+ rspec-core (3.13.0)
66
+ rspec-support (~> 3.13.0)
67
+ rspec-expectations (3.13.1)
68
68
  diff-lcs (>= 1.2.0, < 2.0)
69
- rspec-support (~> 3.7.0)
70
- rspec-mocks (3.7.0)
69
+ rspec-support (~> 3.13.0)
70
+ rspec-mocks (3.13.1)
71
71
  diff-lcs (>= 1.2.0, < 2.0)
72
- rspec-support (~> 3.7.0)
73
- rspec-support (3.7.0)
74
- rspec-wait (0.0.9)
75
- rspec (>= 3, < 4)
72
+ rspec-support (~> 3.13.0)
73
+ rspec-support (3.13.1)
74
+ rspec-wait (0.0.10)
75
+ rspec (>= 3.0)
76
76
  ruby2_keywords (0.0.5)
77
77
  rubyzip (2.3.2)
78
78
  selenium-webdriver (4.1.0)
data/README.md CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  This Ruby gem synchronizes [Capybara](https://github.com/teamcapybara/capybara) commands with client-side JavaScript and AJAX requests. This greatly improves the stability of an end-to-end ("E2E") test suite, even if that suite has timing issues.
4
4
 
5
- The next section explain why your test suite is flaky and how capybara-lockstep can help.\
5
+ The next section explains why your test suite is flaky and how capybara-lockstep can help.\
6
6
  If you don't care you may **skip to [installation instructions](#installation)**.
7
7
 
8
8
 
9
9
  Why are tests flaky?
10
10
  --------------------
11
11
 
12
- A naively written E2E test will have [race conditions](https://makandracards.com/makandra/47336-fixing-flaky-integration-tests) between the test script and the controlled browser. How often these timing issues will fail your test depends on luck and your machine's performance. You may not see these issues for years until a colleague runs your suite on their new laptop.
12
+ A naively written E2E test will have [race conditions](https://makandracards.com/makandra/47336-fixing-flaky-integration-tests) between the test script and the controlled browser. How often these timing issues will cause your tests to fail depends on luck and your machine's performance. You may not see these issues for years until a colleague runs your suite on their new laptop.
13
13
 
14
14
  Here is a typical example for a test that will fail with unlucky timing:
15
15
 
@@ -26,10 +26,10 @@ end
26
26
 
27
27
  This test has four timing issues that may cause it to fail:
28
28
 
29
- 1. We click on the *New tweet* button, but the the JS event handler to open the tweet form wasn't registered yet.
30
- 2. We start filling in the form, but it wasn't loaded yet.
29
+ 1. We click on the *New tweet* button, but the JS event handler to open the tweet form hasn't been registered yet.
30
+ 2. We start filling in the form, but it hasn't been loaded yet.
31
31
  3. After sending the tweet we immediately navigate away, killing the form submission request that is still in flight. Hence the tweet will never appear in the next step.
32
- 4. We look for the new tweet, but the timeline wasn't loaded yet.
32
+ 4. We look for the new tweet, but the timeline hasn't been loaded yet.
33
33
 
34
34
  [Capybara will retry](https://github.com/teamcapybara/capybara#asynchronous-javascript-ajax-and-friends) individual commands or expectations when they fail.\
35
35
  However, only issues **2** and **4** can be healed by retrying.
@@ -71,9 +71,10 @@ When capybara-lockstep synchronizes it will:
71
71
  - wait for client-side JavaScript to render or hydrate DOM elements.
72
72
  - wait for any pending AJAX requests to finish and their callbacks to be called.
73
73
  - wait for dynamically inserted `<script>`s to load (e.g. from [dynamic imports](https://webpack.js.org/guides/code-splitting/#dynamic-imports) or Analytics snippets).
74
- - waits for dynamically inserted `<img>` or `<iframe>` elements to load.
74
+ - wait for dynamically inserted `<img>` or `<iframe>` elements to load (ignoring [lazy-loaded](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#lazy) elements).
75
+ - wait for dynamically inserted `<audio>` and `<video>` elements to load their [metadata](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/loadedmetadata_event)
75
76
 
76
- In summary Capybara can no longer observe or interact with the page while HTTP requests are in flight.
77
+ In summary, Capybara can no longer observe or interact with the page while HTTP requests are in flight.
77
78
  This covers most async work that causes flaky tests.
78
79
 
79
80
 
@@ -83,7 +84,6 @@ Async work not synchronized by capybara-lockstep includes:
83
84
 
84
85
  - Animations
85
86
  - Websocket connections
86
- - Media elements (`<video>`, `<audio>`)
87
87
  - Service workers
88
88
  - Work scheduled via `setTimeout()` or `setInterval()`.
89
89
 
@@ -99,13 +99,14 @@ Check if your application satisfies all requirements for capybara-lockstep:
99
99
 
100
100
  - Capybara 2.0 or higher.
101
101
  - Your Capybara driver must use [selenium-webdriver](https://rubygems.org/gems/selenium-webdriver/) 3.0 or higher. capybara-lockstep deactivates itself for any other driver.
102
+ There is a [fork](https://github.com/Skalar/capybara-lockstep/tree/playwright-driver) with support for [capybara-playwright-driver](https://github.com/YusukeIwaki/capybara-playwright-driver).
102
103
  - This gem was only tested with a Selenium-controlled Chrome browser. [Chrome in headless mode](https://makandracards.com/makandra/492109-running-capybara-tests-in-headless-chrome) is recommended, but not required.
103
104
  - This gem was only tested with Rails, but there's no Rails dependency.
104
105
 
105
106
 
106
107
  ### Installing the Ruby gem
107
108
 
108
- Assuming that you're using Rails Add this line to your application's `Gemfile`:
109
+ Assuming that you're using Rails, add this to your application's `Gemfile`:
109
110
 
110
111
  ```ruby
111
112
  group :test do
@@ -141,9 +142,9 @@ Ideally the snippet should be included in the `<head>` before any other `<script
141
142
 
142
143
  ### Including the middleware (optional)
143
144
 
144
- This gem provides a Rack middleware to block Capybara while your Rails (or Rack) backend is busy.
145
+ This gem provides Rack middleware to block Capybara while your Rails (or Rack) backend is busy.
145
146
 
146
- Using the middleware is optional, as the [JavaScript snippet](#including-the-javascript-snippet-required) already for AJAX requests on the client. However, using the middleware covers some additional edge cases. For example, the middleware detects requests that were aborted on the frontend, but are still being processed by the backend.
147
+ Using the middleware is optional, as the [JavaScript snippet](#including-the-javascript-snippet-required) already waits for asynchronous work on the client. However, using the middleware covers some additional edge cases. For example, the middleware detects requests that were aborted on the frontend, but are still being processed by the backend.
147
148
 
148
149
  To include the middleware in a Rails application, add the following line to `config/environments/test.rb`:
149
150
 
@@ -158,7 +159,23 @@ use Capybara::Lockstep::Middleware
158
159
  # Other middleware here
159
160
  ```
160
161
 
162
+ ### Configuring Selenium WebDriver (recommended)
161
163
 
164
+ By default, webdrivers will automatically dismiss any user prompts (like alerts) when trying to perform an action.
165
+ While capybara-lockstep carefully detects alerts before synchronizing, and will skip interaction with the browser to avoid accidentally dismissing alerts, it can not synchronize around some rare race conditions.
166
+
167
+ [We recommend](https://makandracards.com/makandra/617366-how-to-configure-selenium-webdriver-to-not-automatically-close-alerts-or-other-browser-dialogs) you configure your webdriver to not automatically dismiss user prompts by setting the "unhandled prompt behavior" capability to [`ignore`](https://w3c.github.io/webdriver/#dfn-known-prompt-handling-approaches-table). Using "ignore", errors are raised like with the default behavior, but user prompts are kept open.
168
+
169
+ For example, the Chrome driver can be configured like this:
170
+ ```ruby
171
+ Capybara.register_driver(:selenium) do |app|
172
+ options = Selenium::WebDriver::Chrome::Options.new(
173
+ unhandled_prompt_behavior: 'ignore',
174
+ # ...
175
+ )
176
+ Capybara::Selenium::Driver.new(app, browser: :chrome, options: options)
177
+ end
178
+ ```
162
179
 
163
180
  ### Verify successful integration
164
181
 
@@ -15,7 +15,7 @@ Gem::Specification.new do |spec|
15
15
  spec.metadata["source_code_uri"] = spec.homepage
16
16
 
17
17
  spec.metadata["bug_tracker_uri"] = "https://github.com/makandra/capybara-lockstep/issues"
18
- spec.metadata["changelog_uri"] = "https://github.com/makandra/capybara-lockstep/blob/master/CHANGELOG.md"
18
+ spec.metadata["changelog_uri"] = "https://github.com/makandra/capybara-lockstep/blob/main/CHANGELOG.md"
19
19
  spec.metadata["rubygems_mfa_required"] = 'true'
20
20
 
21
21
  # Specify which files should be added to the gem when it is released.
@@ -3,36 +3,46 @@ require 'ruby2_keywords'
3
3
  module Capybara
4
4
  module Lockstep
5
5
  module SynchronizeMacros
6
+ def self.extended(by)
7
+ by.instance_eval do
8
+ prepend(@synchronize_before_module = Module.new)
9
+ prepend(@synchronize_after_module = Module.new)
10
+ prepend(@unsynchronize_after_module = Module.new)
11
+ end
12
+ end
6
13
 
7
14
  def synchronize_before(meth, lazy:)
8
- mod = Module.new do
15
+ @synchronize_before_module.module_eval do
9
16
  define_method meth do |*args, &block|
10
- Lockstep.auto_synchronize(lazy: lazy, log: "Synchronizing before ##{meth}")
17
+ @synchronize_before_count ||= 0
18
+ @synchronize_before_count += 1
19
+ Lockstep.auto_synchronize(lazy: lazy, log: "Synchronizing before ##{meth}") if @synchronize_before_count == 1
11
20
  super(*args, &block)
21
+ ensure
22
+ @synchronize_before_count -= 1
12
23
  end
13
24
 
14
25
  ruby2_keywords meth
15
26
  end
16
-
17
- prepend(mod)
18
27
  end
19
28
 
20
29
  def synchronize_after(meth)
21
- mod = Module.new do
30
+ @synchronize_after_module.module_eval do
22
31
  define_method meth do |*args, &block|
32
+ @synchronize_after_count ||= 0
33
+ @synchronize_after_count += 1
23
34
  super(*args, &block)
24
35
  ensure
25
- Lockstep.auto_synchronize
36
+ Lockstep.auto_synchronize(log: "Synchronizing after ##{meth}") if @synchronize_after_count == 1
37
+ @synchronize_after_count -= 1
26
38
  end
27
39
 
28
40
  ruby2_keywords meth
29
41
  end
30
-
31
- prepend(mod)
32
42
  end
33
43
 
34
44
  def unsynchronize_after(meth)
35
- mod = Module.new do
45
+ @unsynchronize_after_module.module_eval do
36
46
  define_method meth do |*args, &block|
37
47
  super(*args, &block)
38
48
  ensure
@@ -41,10 +51,7 @@ module Capybara
41
51
 
42
52
  ruby2_keywords meth
43
53
  end
44
-
45
- prepend(mod)
46
54
  end
47
-
48
55
  end
49
56
  end
50
57
  end
@@ -158,23 +165,23 @@ Capybara::Session.class_eval do
158
165
  synchronize_around_script_method :evaluate_async_script
159
166
  end
160
167
 
161
- # Capybara 3 has driver-specific Node classes which sometimes
162
- # super to Capybara::Selenium::Node, but not always.
163
- node_classes = [
168
+ # In Capybara 3 there are the specialized classes for nodes for most browers.
169
+ # We need to patch relevant methods on all of these.
170
+ driver_specific_node_classes = [
164
171
  (Capybara::Selenium::ChromeNode if defined?(Capybara::Selenium::ChromeNode)),
165
172
  (Capybara::Selenium::FirefoxNode if defined?(Capybara::Selenium::FirefoxNode)),
166
173
  (Capybara::Selenium::SafariNode if defined?(Capybara::Selenium::SafariNode)),
167
174
  (Capybara::Selenium::EdgeNode if defined?(Capybara::Selenium::EdgeNode)),
168
175
  (Capybara::Selenium::IENode if defined?(Capybara::Selenium::IENode)),
169
- ].compact
176
+ ].compact.freeze
170
177
 
171
- if node_classes.empty?
172
- # Capybara 2 has no driver-specific Node implementations,
173
- # so we patch the shared base class.
174
- node_classes = [Capybara::Selenium::Node]
175
- end
178
+ # For other browsers (like the :remote browser) we instead get a generic node class.
179
+ # This is also the case for Capybara 2.
180
+ generic_node_classes = [
181
+ Capybara::Selenium::Node,
182
+ ].freeze
176
183
 
177
- node_classes.each do |node_class|
184
+ [*driver_specific_node_classes, *generic_node_classes].each do |node_class|
178
185
  node_class.class_eval do
179
186
  extend Capybara::Lockstep::SynchronizeMacros
180
187
 
@@ -177,39 +177,47 @@ window.CapybaraLockstep = (function() {
177
177
  }
178
178
 
179
179
  function isRemoteScript(element) {
180
- if (element.tagName === 'SCRIPT') {
181
- let src = element.getAttribute('src')
182
- let type = element.getAttribute('type')
180
+ return element.matches('script[src]') && !hasDataSource(element)
181
+ }
183
182
 
184
- return src && (!type || /javascript/i.test(type))
185
- }
183
+ function isTrackableImage(element) {
184
+ return element.matches('img') &&
185
+ !element.complete &&
186
+ !hasDataSource(element) &&
187
+ element.getAttribute('loading') !== 'lazy'
186
188
  }
187
189
 
188
- function isRemoteImage(element) {
189
- if (element.tagName === 'IMG' && !element.complete) {
190
- let src = element.getAttribute('src')
191
- let srcSet = element.getAttribute('srcset')
190
+ function isTrackableIFrame(element) {
191
+ return element.matches('iframe') &&
192
+ !hasDataSource(element) &&
193
+ element.getAttribute('loading') !== 'lazy'
194
+ }
195
+
196
+ function hasDataSource(element) {
197
+ // <img> can have <img src> and <img srcset>
198
+ // <video> can have <video src> or <video><source src>
199
+ // <audio> can have <audio src> or <audio><source src>
200
+ return element.matches('[src*="data:"], [srcset*="data:"]') ||
201
+ !!element.querySelector('source [src*="data:"], source [srcset*="data:"]')
202
+ }
192
203
 
193
- let localSrcPattern = /^data:/
194
- let localSrcSetPattern = /(^|\s)data:/
204
+ function isTrackableMediaElement(element) {
205
+ return element.matches('audio, video') &&
206
+ element.readyState === 0 && // no metadata known
207
+ !hasDataSource(element) &&
208
+ element.getAttribute('preload') !== 'none'
209
+ }
195
210
 
196
- let hasLocalSrc = src && localSrcPattern.test(src)
197
- let hasLocalSrcSet = srcSet && localSrcSetPattern.test(srcSet)
211
+ function trackRemoteElement(element, condition, workTag) {
212
+ trackLoadingElement(element, condition, workTag, 'load', 'error')
198
213
 
199
- return (src && !hasLocalSrc) || (srcSet && !hasLocalSrcSet)
200
- }
201
214
  }
202
215
 
203
- function isRemoteInlineFrame(element) {
204
- if (element.tagName === 'IFRAME') {
205
- let src = element.getAttribute('src')
206
- let localSrcPattern = /^data:/
207
- let hasLocalSrc = src && localSrcPattern.test(src)
208
- return (src && !hasLocalSrc)
209
- }
216
+ function trackMediaElement(element, condition, workTag) {
217
+ trackLoadingElement(element, condition, workTag, 'loadedmetadata', 'error')
210
218
  }
211
219
 
212
- function trackRemoteElement(element, condition, workTag) {
220
+ function trackLoadingElement(element, condition, workTag, loadEvent, errorEvent) {
213
221
  if (!condition(element)) {
214
222
  return
215
223
  }
@@ -220,8 +228,8 @@ window.CapybaraLockstep = (function() {
220
228
 
221
229
  let doStop = function() {
222
230
  stopped = true
223
- element.removeEventListener('load', doStop)
224
- element.removeEventListener('error', doStop)
231
+ element.removeEventListener(loadEvent, doStop)
232
+ element.removeEventListener(errorEvent, doStop)
225
233
  stopWork(workTag)
226
234
  }
227
235
 
@@ -240,11 +248,11 @@ window.CapybaraLockstep = (function() {
240
248
  }
241
249
 
242
250
  let scheduleCheckCondition = function() {
243
- setTimeout(checkCondition, 200)
251
+ setTimeout(checkCondition, 150)
244
252
  }
245
253
 
246
- element.addEventListener('load', doStop)
247
- element.addEventListener('error', doStop)
254
+ element.addEventListener(loadEvent, doStop)
255
+ element.addEventListener(errorEvent, doStop)
248
256
 
249
257
  // We periodically check whether we still think the element will
250
258
  // produce a `load` or `error` event.
@@ -256,8 +264,9 @@ window.CapybaraLockstep = (function() {
256
264
  change.addedNodes.forEach(function(addedNode) {
257
265
  if (addedNode.nodeType === Node.ELEMENT_NODE) {
258
266
  trackRemoteElement(addedNode, isRemoteScript, 'Script')
259
- trackRemoteElement(addedNode, isRemoteImage, 'Image')
260
- trackRemoteElement(addedNode, isRemoteInlineFrame, 'Inline frame')
267
+ trackRemoteElement(addedNode, isTrackableImage, 'Image')
268
+ trackRemoteElement(addedNode, isTrackableIFrame, 'Inline frame')
269
+ trackMediaElement(addedNode, isTrackableMediaElement, 'Media element')
261
270
  }
262
271
  })
263
272
  })
@@ -1,5 +1,5 @@
1
1
  module Capybara
2
2
  module Lockstep
3
- VERSION = "2.1.0"
3
+ VERSION = "2.2.1"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: capybara-lockstep
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Henning Koch
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-01-10 00:00:00.000000000 Z
11
+ date: 2024-06-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: capybara
@@ -107,7 +107,7 @@ metadata:
107
107
  homepage_uri: https://github.com/makandra/capybara-lockstep
108
108
  source_code_uri: https://github.com/makandra/capybara-lockstep
109
109
  bug_tracker_uri: https://github.com/makandra/capybara-lockstep/issues
110
- changelog_uri: https://github.com/makandra/capybara-lockstep/blob/master/CHANGELOG.md
110
+ changelog_uri: https://github.com/makandra/capybara-lockstep/blob/main/CHANGELOG.md
111
111
  rubygems_mfa_required: 'true'
112
112
  post_install_message:
113
113
  rdoc_options: []
@@ -124,7 +124,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
124
124
  - !ruby/object:Gem::Version
125
125
  version: '0'
126
126
  requirements: []
127
- rubygems_version: 3.4.3
127
+ rubygems_version: 3.4.1
128
128
  signing_key:
129
129
  specification_version: 4
130
130
  summary: Synchronize Capybara commands with client-side JavaScript and AJAX requests