capybara-lockstep 1.0.0 → 1.1.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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +6 -0
- data/CHANGELOG.md +56 -20
- data/Gemfile.lock +1 -1
- data/README.md +107 -129
- data/lib/capybara-lockstep/configuration.rb +9 -1
- data/lib/capybara-lockstep/errors.rb +1 -1
- data/lib/capybara-lockstep/helper.js +21 -50
- data/lib/capybara-lockstep/lockstep.rb +10 -3
- data/lib/capybara-lockstep/version.rb +1 -1
- metadata +2 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 4ece5e0daeb6883b02791ed06f6ff2afca446e5aa5635f22d3ec5b94d0e96aa7
         | 
| 4 | 
            +
              data.tar.gz: 5e4d53a8c59f71071aa48fee8ec87fa62e5c189e026b5cac563fd9427a082c59
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 7e68bf91ab8b5ecf91b145159697e7990dbbbb3a5497955d50d91ea83d802c50195f803ae1fd029b2b76dc18b407bd1995b03aa292e30873cd35f8746dfa509e
         | 
| 7 | 
            +
              data.tar.gz: 8fb8d9b19fc47b5769208d36125b6cc2fb40375a8d5b46216ec318a3fd8459ca26f6bf22f352efcf350b7d7cbfe89f7bb866633fb4e096812b7d31513d4c7bae
         | 
    
        data/.github/workflows/test.yml
    CHANGED
    
    | @@ -28,6 +28,12 @@ jobs: | |
| 28 28 | 
             
                  BUNDLE_GEMFILE: "${{ matrix.gemfile }}"
         | 
| 29 29 | 
             
                steps:
         | 
| 30 30 | 
             
                - uses: actions/checkout@v2
         | 
| 31 | 
            +
                - name: Install Chrome
         | 
| 32 | 
            +
                  uses: browser-actions/setup-chrome@latest
         | 
| 33 | 
            +
                - name: Show Chrome version
         | 
| 34 | 
            +
                  run: chrome --version
         | 
| 35 | 
            +
                - name: Install ChromeDriver
         | 
| 36 | 
            +
                  uses: nanasess/setup-chromedriver@master
         | 
| 31 37 | 
             
                - name: Install ruby
         | 
| 32 38 | 
             
                  uses: ruby/setup-ruby@v1
         | 
| 33 39 | 
             
                  with:
         | 
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -2,32 +2,68 @@ 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 | 
            +
            ## 1.1.0
         | 
| 5 6 |  | 
| 6 | 
            -
             | 
| 7 | 
            +
            - Stop handling of `[data-initializing]` attribute. Apps that have late initialization after the `load` event can just use `CapybaraLockstep.startWork()`.
         | 
| 8 | 
            +
            - Remove useless tracking of interaction events like `"click"` or `"focus"`. If such an event handler would start an AJAX request, it is already tracked.
         | 
| 9 | 
            +
            - On apps with Unpoly 0.x, wait for one more task after `DOMContentLoaded`. Please upgrade to Unpoly 1.x or 2.x, as this logic will be removed in a year or so.
         | 
| 7 10 |  | 
| 8 | 
            -
             | 
| 11 | 
            +
            ## 1.0.0
         | 
| 9 12 |  | 
| 10 | 
            -
            - 
         | 
| 13 | 
            +
            - First stable release.
         | 
| 14 | 
            +
            - Replace option `Capybara::Lockstep.config` (`true`, `false`) with a more refined option `.mode` (`:auto`, `:manual`, `:off`)
         | 
| 11 15 |  | 
| 12 | 
            -
             | 
| 16 | 
            +
            ## 0.7.0
         | 
| 13 17 |  | 
| 14 | 
            -
            -
         | 
| 18 | 
            +
            - Ruby 3 compatibility.
         | 
| 19 | 
            +
            - Fix logging.
         | 
| 15 20 |  | 
| 16 | 
            -
            ## 0. | 
| 21 | 
            +
            ## 0.6.0
         | 
| 17 22 |  | 
| 18 | 
            -
             | 
| 23 | 
            +
            - Synchronize around `evaluate_script` and `execute_script`.
         | 
| 24 | 
            +
            - Improve logging.
         | 
| 19 25 |  | 
| 20 | 
            -
             | 
| 21 | 
            -
            - add gemika for tests with github actions
         | 
| 22 | 
            -
            - add Ruby 3 support
         | 
| 26 | 
            +
            ## 0.5.0
         | 
| 23 27 |  | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
             | 
| 29 | 
            -
            ## 0. | 
| 30 | 
            -
             | 
| 31 | 
            -
             | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 28 | 
            +
            - Allow developer to signal custom async work.
         | 
| 29 | 
            +
            - Option to wait additional tasks, to handle legacy promise implementations.
         | 
| 30 | 
            +
            - Debugging log can be enabled during a running test.
         | 
| 31 | 
            +
            - Also wait for images and iframes.
         | 
| 32 | 
            +
             | 
| 33 | 
            +
            ## 0.4.0
         | 
| 34 | 
            +
             | 
| 35 | 
            +
            - Don't fail the test when synchronization times out.
         | 
| 36 | 
            +
            - Capybara::Lockstep.debug = true will now also enable client-side logging on the browser's JavaScript console.
         | 
| 37 | 
            +
            - Always wait at least for `Capybara.default_max_wait_time`.
         | 
| 38 | 
            +
             | 
| 39 | 
            +
            ## 0.3.2
         | 
| 40 | 
            +
             | 
| 41 | 
            +
            - Delay synchronization when an alert is open (instead of failing)
         | 
| 42 | 
            +
             | 
| 43 | 
            +
             | 
| 44 | 
            +
            ## 0.3.1
         | 
| 45 | 
            +
             | 
| 46 | 
            +
            - Fix typo in log message
         | 
| 47 | 
            +
             | 
| 48 | 
            +
            ## 0.3.0
         | 
| 49 | 
            +
             | 
| 50 | 
            +
            - Rework entire waiting logic to be lazy.
         | 
| 51 | 
            +
            - There is now a single method `Capybara::Lockstep.synchronize` (no distinction between awaiting "initialization" and "idle").
         | 
| 52 | 
            +
             | 
| 53 | 
            +
            ## 0.2.3
         | 
| 54 | 
            +
             | 
| 55 | 
            +
            - When we cannot wait for browser idle due to an open alert, wait before the next Capybara synchronize
         | 
| 56 | 
            +
             | 
| 57 | 
            +
            ## 0.2.2
         | 
| 58 | 
            +
             | 
| 59 | 
            +
            - Fix incorrect data in gemspec.
         | 
| 60 | 
            +
             | 
| 61 | 
            +
             | 
| 62 | 
            +
            ## 0.2.1
         | 
| 63 | 
            +
             | 
| 64 | 
            +
            - Internal changes.
         | 
| 65 | 
            +
             | 
| 66 | 
            +
             | 
| 67 | 
            +
            ## 0.2.0
         | 
| 68 | 
            +
             | 
| 69 | 
            +
            - Initial release.
         | 
    
        data/Gemfile.lock
    CHANGED
    
    
    
        data/README.md
    CHANGED
    
    | @@ -1,12 +1,15 @@ | |
| 1 1 | 
             
            # capybara-lockstep
         | 
| 2 2 |  | 
| 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  | 
| 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 | 
            +
             | 
| 5 | 
            +
            The next section explain why your test suite is flaky and how capybara-lockstep can help.\
         | 
| 6 | 
            +
            If you don't care you may skip to [installation instructions](#installation).
         | 
| 4 7 |  | 
| 5 8 |  | 
| 6 9 | 
             
            Why are tests flaky?
         | 
| 7 10 | 
             
            --------------------
         | 
| 8 11 |  | 
| 9 | 
            -
            A naively written  | 
| 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.
         | 
| 10 13 |  | 
| 11 14 | 
             
            Here is a typical example for a test that will fail with unlucky timing:
         | 
| 12 15 |  | 
| @@ -23,28 +26,50 @@ end | |
| 23 26 |  | 
| 24 27 | 
             
            This test has four timing issues that may cause it to fail:
         | 
| 25 28 |  | 
| 26 | 
            -
            1. We click on the  | 
| 29 | 
            +
            1. We click on the *New tweet* button, but the the JS event handler to open the tweet form wasn't registered yet.
         | 
| 27 30 | 
             
            2. We start filling in the form, but it wasn't loaded yet.
         | 
| 28 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.
         | 
| 29 32 | 
             
            4. We look for the new tweet, but the timeline wasn't loaded yet.
         | 
| 30 33 |  | 
| 31 | 
            -
            Capybara will retry individual commands or expectations when they fail | 
| 34 | 
            +
            [Capybara will retry](https://github.com/teamcapybara/capybara#asynchronous-javascript-ajax-and-friends) individual commands or expectations when they fail.\
         | 
| 35 | 
            +
            However, only issues **2** and **4** can be healed by retrying.
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            While it is [possible](https://makandracards.com/makandra/47336-fixing-flaky-integration-tests) to remove most of the timing issues above, it requires skill and discipline.\
         | 
| 38 | 
            +
            capybara-lockstep fixes issues **1**, **2**, **3** and **4** without any changes to the test code.
         | 
| 39 | 
            +
             | 
| 40 | 
            +
             | 
| 41 | 
            +
            ### This is a JavaScript problem
         | 
| 42 | 
            +
             | 
| 43 | 
            +
            The timing issues above will only manifest in an app where links, forms and buttons are handled by JavaScript.
         | 
| 44 | 
            +
             | 
| 45 | 
            +
            When all you have is standard HTML links and forms, stock Capybara will not see timing issues:
         | 
| 46 | 
            +
             | 
| 47 | 
            +
            - After a `visit()` Capybara/WebDriver will wait until the page is completely loaded
         | 
| 48 | 
            +
            - When following a link Capybara/WebDriver will wait until the link destination is completely loaded
         | 
| 49 | 
            +
            - When submitting a form Capybara/WebDriver will wait until the response is completely loaded
         | 
| 50 | 
            +
             | 
| 51 | 
            +
            However, when JavaScript handles a link click, you get **zero guarantees**.\
         | 
| 52 | 
            +
            Capybara/WebDriver **will not wait** for AJAX requests or any other async work.
         | 
| 32 53 |  | 
| 33 | 
            -
            While it is [possible](https://makandracards.com/makandra/47336-fixing-flaky-integration-tests) to remove most of the timing issues above, it requires skill and discipline. capybara-lockstep fixes issues **1**, **2**, **3** and **4** without any changes to the test code.
         | 
| 34 54 |  | 
| 35 55 |  | 
| 36 56 | 
             
            How capybara-lockstep helps
         | 
| 37 57 | 
             
            ---------------------------
         | 
| 38 58 |  | 
| 39 | 
            -
            capybara-lockstep waits until the browser is idle before moving on to the next Capybara command. This greatly relieves the pressure on Capybara's retry logic.
         | 
| 59 | 
            +
            capybara-lockstep waits until the browser is idle before moving on to the next Capybara command. This greatly relieves the pressure on [Capybara's retry logic](https://github.com/teamcapybara/capybara#asynchronous-javascript-ajax-and-friends).
         | 
| 40 60 |  | 
| 41 | 
            -
             | 
| 61 | 
            +
            Before Capybara simulates a user interaction (clicking, typing, etc.) or before it visits a new URL:
         | 
| 42 62 |  | 
| 43 | 
            -
            - capybara-lockstep waits for all document resources to load.
         | 
| 63 | 
            +
            - capybara-lockstep waits for all document resources to load (images, CSS, fonts, frames).
         | 
| 64 | 
            +
            - capybara-lockstep waits for any AJAX requests to finish.
         | 
| 44 65 | 
             
            - capybara-lockstep waits for client-side JavaScript to render or hydrate DOM elements.
         | 
| 45 | 
            -
            - capybara-lockstep waits for any AJAX requests.
         | 
| 46 66 | 
             
            - capybara-lockstep waits for dynamically inserted `<script>`s to load (e.g. from [dynamic imports](https://webpack.js.org/guides/code-splitting/#dynamic-imports) or Analytics snippets).
         | 
| 47 | 
            -
            - capybara-lockstep waits for dynamically `<img>` or `<iframe>` elements to load.
         | 
| 67 | 
            +
            - capybara-lockstep waits for dynamically inserted `<img>` or `<iframe>` elements to load.
         | 
| 68 | 
            +
             | 
| 69 | 
            +
            In summary Capybara can no longer observe the page while HTTP requests are in flight.
         | 
| 70 | 
            +
            This covers most async work that causes flaky tests.
         | 
| 71 | 
            +
             | 
| 72 | 
            +
            You can also configure capybara-lockstep to [wait for other async work](#signaling-asynchronous-work) that does not involve the network, like animations.
         | 
| 48 73 |  | 
| 49 74 |  | 
| 50 75 | 
             
            Installation
         | 
| @@ -76,127 +101,82 @@ And then execute: | |
| 76 101 | 
             
            $ bundle install
         | 
| 77 102 | 
             
            ```
         | 
| 78 103 |  | 
| 79 | 
            -
            If you're not using Rails you should also `require 'capybara-lockstep'` in your `spec_helper.rb` (RSpec) or `env.rb` (Cucumber).
         | 
| 104 | 
            +
            If you're not using Rails you should also `require 'capybara-lockstep'` in your `spec_helper.rb` (RSpec), `test_helper.rb` (Minitest) or `env.rb` (Cucumber).
         | 
| 80 105 |  | 
| 81 106 |  | 
| 82 107 | 
             
            ### Including the JavaScript snippet
         | 
| 83 108 |  | 
| 84 109 | 
             
            capybara-lockstep requires a JavaScript snippet to be embedded by the application under test. If that snippet is missing on a screen, capybara-lockstep will not be able to synchronize with the browser. In that case the test will continue without synchronization.
         | 
| 85 110 |  | 
| 86 | 
            -
            If you're using Rails you can use the `capybara_lockstep` helper to insert the snippet into your application layouts:
         | 
| 111 | 
            +
            **If you're using Rails** you can use the `capybara_lockstep` helper to insert the snippet into your application layouts:
         | 
| 87 112 |  | 
| 88 113 | 
             
            ```erb
         | 
| 89 114 | 
             
            <%= capybara_lockstep if Rails.env.test? %>
         | 
| 90 115 | 
             
            ```
         | 
| 91 116 |  | 
| 92 | 
            -
            Ideally the snippet should be included in the `<head>` before any other `<script>` tags. | 
| 117 | 
            +
            Ideally the snippet should be included in the `<head>` before any other `<script>` tags.
         | 
| 93 118 |  | 
| 94 | 
            -
            If you  | 
| 119 | 
            +
            **If you're not using Rails** you can `include Capybara::Lockstep::Helper` and access the JavaScript with `capybara_lockstep_script`.
         | 
| 95 120 |  | 
| 96 | 
            -
            If you | 
| 121 | 
            +
            **If you have a strict [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)** the `capybara_lockstep` Rails helper will insert a CSP nonce by default. You can also pass an explicit nonce string using the `:nonce` option.
         | 
| 97 122 |  | 
| 98 123 |  | 
| 99 | 
            -
            ### Signaling the end of page initialization
         | 
| 100 124 |  | 
| 101 | 
            -
             | 
| 125 | 
            +
            ### Verify successful integration
         | 
| 102 126 |  | 
| 103 | 
            -
            capybara-lockstep will  | 
| 127 | 
            +
            capybara-lockstep will automatically patch Capybara to wait for the browser after every command.
         | 
| 104 128 |  | 
| 105 | 
            -
             | 
| 129 | 
            +
            Run your test suite to see if integration was successful and whether stability improves. During validation we recommend to activate the [debugging log](#debugging-log) before your test:
         | 
| 106 130 |  | 
| 107 | 
            -
            ``` | 
| 108 | 
            -
             | 
| 131 | 
            +
            ```ruby
         | 
| 132 | 
            +
            Capybara::Lockstep.debug = true
         | 
| 109 133 | 
             
            ```
         | 
| 110 134 |  | 
| 111 | 
            -
             | 
| 135 | 
            +
            You should see messages like this in your console:
         | 
| 112 136 |  | 
| 113 | 
            -
             | 
| 114 | 
            -
             | 
| 115 | 
            -
             | 
| 137 | 
            +
            ```text
         | 
| 138 | 
            +
            [capybara-lockstep] Synchronizing
         | 
| 139 | 
            +
            [capybara-lockstep] Finished waiting for JavaScript
         | 
| 140 | 
            +
            [capybara-lockstep] Synchronized successfully
         | 
| 141 | 
            +
            ```
         | 
| 116 142 |  | 
| 117 | 
            -
             | 
| 143 | 
            +
            Note that you may see some failures from tests with wrong assertions, which previously passed due to lucky timing.
         | 
| 118 144 |  | 
| 119 | 
            -
            If all your initializing JavaScript runs synchronously on `DOMContentLoaded`, you can remove `[data-initializing]` in an event handler:
         | 
| 120 145 |  | 
| 121 | 
            -
            ```js
         | 
| 122 | 
            -
            document.addEventListener('DOMContentLoaded', function() {
         | 
| 123 | 
            -
              // Initialize the page here
         | 
| 124 | 
            -
              document.body.removeAttribute('data-initializing')
         | 
| 125 | 
            -
            })
         | 
| 126 | 
            -
            ```
         | 
| 127 146 |  | 
| 128 | 
            -
             | 
| 147 | 
            +
            ## Signaling asynchronous work
         | 
| 129 148 |  | 
| 130 | 
            -
             | 
| 131 | 
            -
            document.addEventListener('DOMContentLoaded', function() {
         | 
| 132 | 
            -
              Libary.initialize({
         | 
| 133 | 
            -
                onFinished: function() {
         | 
| 134 | 
            -
                  document.body.removeAttribute('data-initializing')
         | 
| 135 | 
            -
                }
         | 
| 136 | 
            -
              })
         | 
| 137 | 
            -
            })
         | 
| 138 | 
            -
            ```
         | 
| 149 | 
            +
            By default capybara-lockstep blocks all async work that 
         | 
| 139 150 |  | 
| 140 | 
            -
             | 
| 151 | 
            +
            If for some reason you want capybara-lockstep to consider additional asynchronous work as "busy", you can do so:
         | 
| 141 152 |  | 
| 142 153 | 
             
            ```js
         | 
| 143 | 
            -
             | 
| 144 | 
            -
             | 
| 145 | 
            -
               | 
| 154 | 
            +
            CapybaraLockstep.startWork('Eject warp core')
         | 
| 155 | 
            +
            doAsynchronousWork().then(function() {
         | 
| 156 | 
            +
              CapybaraLockstep.stopWork('Eject warp core')
         | 
| 146 157 | 
             
            })
         | 
| 147 158 | 
             
            ```
         | 
| 148 159 |  | 
| 149 | 
            -
             | 
| 160 | 
            +
            The string argument is used for logging (when logging is enabled). It does **not** need to be unique per job. In this case you should see messages like this in your browser's JavaScript console:
         | 
| 150 161 |  | 
| 151 | 
            -
            ``` | 
| 152 | 
            -
             | 
| 153 | 
            -
             | 
| 154 | 
            -
                HugeLibrary.initialize()
         | 
| 155 | 
            -
                document.body.removeAttribute('data-initializing')
         | 
| 156 | 
            -
              })
         | 
| 157 | 
            -
            })
         | 
| 162 | 
            +
            ```text
         | 
| 163 | 
            +
            [capybara-lockstep] Started work: Eject warp core [1 jobs]
         | 
| 164 | 
            +
            [capybara-lockstep] Finished work: Eject warp core [0 jobs]
         | 
| 158 165 | 
             
            ```
         | 
| 159 166 |  | 
| 167 | 
            +
            You may omit the string argument, in which case nothing will be logged, but the work will still be tracked.
         | 
| 160 168 |  | 
| 161 | 
            -
            #### Example: Unpoly
         | 
| 162 | 
            -
             | 
| 163 | 
            -
            When you're using [Unpoly](https://unpoly.com/) initializing will usually happen synchronously in [compilers](https://unpoly.com/up.compiler). Hence a compiler is a good place to remove `[data-initializing]`:
         | 
| 164 | 
            -
             | 
| 165 | 
            -
            ```js
         | 
| 166 | 
            -
            up.compiler('body', function(body) {
         | 
| 167 | 
            -
              body.removeAttribute('data-initializing')
         | 
| 168 | 
            -
            })
         | 
| 169 | 
            -
            ```
         | 
| 170 169 |  | 
| 171 | 
            -
             | 
| 170 | 
            +
            ## Note on interacting with the JavaScript API
         | 
| 172 171 |  | 
| 173 | 
            -
             | 
| 172 | 
            +
            If you only load capybara-lockstep in tests you, should check for the `CapybaraLockstep` global to be defined before you interact with the JavaScript API.
         | 
| 174 173 |  | 
| 175 174 | 
             
            ```js
         | 
| 176 | 
            -
             | 
| 177 | 
            -
               | 
| 178 | 
            -
             | 
| 179 | 
            -
                link: function() {
         | 
| 180 | 
            -
                  document.body.removeAttribute('data-initializing')
         | 
| 181 | 
            -
                }
         | 
| 182 | 
            -
              }
         | 
| 183 | 
            -
            })
         | 
| 184 | 
            -
            ```
         | 
| 185 | 
            -
             | 
| 186 | 
            -
            ### Verify successful integration
         | 
| 187 | 
            -
             | 
| 188 | 
            -
            capybara-lockstep will automatically patch Capybara to wait for the browser after every command.
         | 
| 189 | 
            -
             | 
| 190 | 
            -
            Run your test suite to see if integration was successful and whether stability improves. During validation we recommend to activate `Capybara::Lockstep.debug = true` in your `spec_helper.rb` (RSpec) or `env.rb` (Cucumber). You should see messages like this in your console:
         | 
| 191 | 
            -
             | 
| 192 | 
            -
            ```text
         | 
| 193 | 
            -
            [capybara-lockstep] Synchronizing
         | 
| 194 | 
            -
            [capybara-lockstep] Finished waiting for JavaScript
         | 
| 195 | 
            -
            [capybara-lockstep] Synchronized successfully
         | 
| 175 | 
            +
            if (window.CapybaraLockstep) {
         | 
| 176 | 
            +
              // interact with CapybaraLockstep
         | 
| 177 | 
            +
            }
         | 
| 196 178 | 
             
            ```
         | 
| 197 179 |  | 
| 198 | 
            -
            Note that you may see some failures from tests with wrong assertions, which sometimes passed due to lucky timing.
         | 
| 199 | 
            -
             | 
| 200 180 |  | 
| 201 181 | 
             
            ## Performance impact
         | 
| 202 182 |  | 
| @@ -204,7 +184,7 @@ capybara-lockstep may or may not impact the runtime of your test suite. It depen | |
| 204 184 |  | 
| 205 185 | 
             
            While waiting for the browser to be idle does take a few milliseconds, Capybara no longer needs to retry failed commands. You will also save time from not needing to re-run failed tests.
         | 
| 206 186 |  | 
| 207 | 
            -
            In casual testing I experienced a performance impact between +/- 10%.
         | 
| 187 | 
            +
            In casual testing with large test suites I experienced a performance impact between +/- 10%.
         | 
| 208 188 |  | 
| 209 189 |  | 
| 210 190 | 
             
            ## Debugging log
         | 
| @@ -243,9 +223,9 @@ Capybara::Lockstep.debug = Rails.logger | |
| 243 223 |  | 
| 244 224 | 
             
            ### Logging in the browser only
         | 
| 245 225 |  | 
| 246 | 
            -
            To enable logging in the browser console (but not STDOUT), include the snippet with `{ debug: true }`:
         | 
| 226 | 
            +
            To enable logging in the browser console (but not STDOUT), include the [JavaScript snippet](#including-the-javascript-snippet) with `{ debug: true }`:
         | 
| 247 227 |  | 
| 248 | 
            -
            ```
         | 
| 228 | 
            +
            ```ruby
         | 
| 249 229 | 
             
            capybara_lockstep(debug: true)
         | 
| 250 230 | 
             
            ```
         | 
| 251 231 |  | 
| @@ -253,7 +233,11 @@ capybara_lockstep(debug: true) | |
| 253 233 |  | 
| 254 234 | 
             
            By default capybara-lockstep will wait `Capybara.default_max_wait_time` seconds for the page initialize and for JavaScript and AJAX request to finish.
         | 
| 255 235 |  | 
| 256 | 
            -
            When synchronization times out, capybara-lockstep will log | 
| 236 | 
            +
            When synchronization times out, capybara-lockstep will [log](#debugging-log):
         | 
| 237 | 
            +
             | 
| 238 | 
            +
            ```text
         | 
| 239 | 
            +
            [capybara-lockstep] Could not synchronize within 3 seconds
         | 
| 240 | 
            +
            ```
         | 
| 257 241 |  | 
| 258 242 | 
             
            You can configure a different timeout:
         | 
| 259 243 |  | 
| @@ -261,10 +245,19 @@ You can configure a different timeout: | |
| 261 245 | 
             
            Capybara::Lockstep.timeout = 5 # seconds
         | 
| 262 246 | 
             
            ```
         | 
| 263 247 |  | 
| 264 | 
            -
             | 
| 248 | 
            +
            By default Capybara will **not** raise an error after a timeout. You may occasionally get a slow server response, and Capybara will retry synchronization before the next interaction or `visit`. This is often good enough.
         | 
| 249 | 
            +
             | 
| 250 | 
            +
            If you want to be strict you may configure that an `Capybara::Lockstep::Timeout` error is raised after a timeout:
         | 
| 251 | 
            +
             | 
| 252 | 
            +
            ```ruby
         | 
| 253 | 
            +
            Capybara::Lockstep.timeout_with = :error
         | 
| 254 | 
            +
            ```
         | 
| 255 | 
            +
             | 
| 256 | 
            +
            To revert to defaults:
         | 
| 265 257 |  | 
| 266 258 | 
             
            ```ruby
         | 
| 267 259 | 
             
            Capybara::Lockstep.timeout = nil
         | 
| 260 | 
            +
            Capybara::Lockstep.timeout_with = nil
         | 
| 268 261 | 
             
            ```
         | 
| 269 262 |  | 
| 270 263 |  | 
| @@ -300,7 +293,7 @@ ensure | |
| 300 293 | 
             
            end
         | 
| 301 294 | 
             
            ```
         | 
| 302 295 |  | 
| 303 | 
            -
             | 
| 296 | 
            +
            In the `:manual` mode you may still force synchronization by calling `Capybara::Lockstep.synchronize` manually.
         | 
| 304 297 |  | 
| 305 298 | 
             
            To completely disable synchronization:
         | 
| 306 299 |  | 
| @@ -310,36 +303,6 @@ Capybara::Lockstep.synchronize # will not synchronize | |
| 310 303 | 
             
            ```
         | 
| 311 304 |  | 
| 312 305 |  | 
| 313 | 
            -
            ## Signaling asynchronous work
         | 
| 314 | 
            -
             | 
| 315 | 
            -
            If for some reason you want capybara-lockstep to consider additional asynchronous work as "busy", you can do so:
         | 
| 316 | 
            -
             | 
| 317 | 
            -
            ```js
         | 
| 318 | 
            -
            CapybaraLockstep.startWork('Eject warp core')
         | 
| 319 | 
            -
            doAsynchronousWork().then(function() {
         | 
| 320 | 
            -
              CapybaraLockstep.stopWork('Eject warp core')
         | 
| 321 | 
            -
            })
         | 
| 322 | 
            -
            ```
         | 
| 323 | 
            -
             | 
| 324 | 
            -
            The string argument is used for logging (when logging is enabled). It does **not** need to be unique per job. In this case you should see messages like this in your browser's JavaScript console:
         | 
| 325 | 
            -
             | 
| 326 | 
            -
            ```text
         | 
| 327 | 
            -
            [capybara-lockstep] Started work: Eject warp core [1 jobs]
         | 
| 328 | 
            -
            [capybara-lockstep] Finished work: Eject warp core [0 jobs]
         | 
| 329 | 
            -
            ```
         | 
| 330 | 
            -
             | 
| 331 | 
            -
            You may omit the string argument, in which case nothing will be logged, but the work will still be tracked.
         | 
| 332 | 
            -
             | 
| 333 | 
            -
             | 
| 334 | 
            -
            ## Note on interacting with the JavaScript API
         | 
| 335 | 
            -
             | 
| 336 | 
            -
            If you only load capybara-lockstep in tests you, should check for the `CapybaraLockstep` global to be defined before you interact with the JavaScript API.
         | 
| 337 | 
            -
             | 
| 338 | 
            -
            ```js
         | 
| 339 | 
            -
            if (window.CapybaraLockstep) {
         | 
| 340 | 
            -
              // interact with CapybaraLockstep
         | 
| 341 | 
            -
            }
         | 
| 342 | 
            -
            ```
         | 
| 343 306 |  | 
| 344 307 | 
             
            ## Handling legacy promises
         | 
| 345 308 |  | 
| @@ -364,16 +327,31 @@ If you see longer `then()` chains in your code, you may need to configure a high | |
| 364 327 | 
             
            This will have a negative performance impact on your test suite.
         | 
| 365 328 |  | 
| 366 329 |  | 
| 367 | 
            -
            ##  | 
| 330 | 
            +
            ## Contributing
         | 
| 368 331 |  | 
| 369 | 
            -
             | 
| 332 | 
            +
            Pull requests are welcome on GitHub at <https://github.com/makandra/capybara-lockstep>.
         | 
| 370 333 |  | 
| 371 | 
            -
             | 
| 334 | 
            +
            After checking out the repo, run `bin/setup` to install dependencies.
         | 
| 372 335 |  | 
| 336 | 
            +
            Then, run `rake spec` to run the tests.
         | 
| 373 337 |  | 
| 374 | 
            -
             | 
| 338 | 
            +
            You can also run `bin/console` for an interactive prompt that will allow you to experiment.
         | 
| 375 339 |  | 
| 376 | 
            -
             | 
| 340 | 
            +
            ### Manually testing a change
         | 
| 341 | 
            +
             | 
| 342 | 
            +
            To test an unrelased change with a test suite, we recommend to temporarily link the local repository from your test suites's `Gemfile`:
         | 
| 343 | 
            +
             | 
| 344 | 
            +
            ```ruby
         | 
| 345 | 
            +
            gem 'capybara-lockstep', path: '../capybara-lockstep'
         | 
| 346 | 
            +
            ```
         | 
| 347 | 
            +
             | 
| 348 | 
            +
            As an alternative you may also install this gem onto your local machine by running `bundle exec rake install`.
         | 
| 349 | 
            +
             | 
| 350 | 
            +
            ### Releasing a new version
         | 
| 351 | 
            +
             | 
| 352 | 
            +
            - Update the version number in `version.rb`
         | 
| 353 | 
            +
             - Run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
         | 
| 354 | 
            +
             - If RubyGems publishing seems to freeze, try entering your OTP code.
         | 
| 377 355 |  | 
| 378 356 |  | 
| 379 357 | 
             
            ## License
         | 
| @@ -10,6 +10,14 @@ module Capybara | |
| 10 10 | 
             
                    @timeout = seconds
         | 
| 11 11 | 
             
                  end
         | 
| 12 12 |  | 
| 13 | 
            +
                  def timeout_with
         | 
| 14 | 
            +
                    @timeout_with.nil? ? :log : @timeout_with
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  def timeout_with=(action)
         | 
| 18 | 
            +
                    @timeout_with = action&.to_sym
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 13 21 | 
             
                  def debug?
         | 
| 14 22 | 
             
                    # @debug may also be a Logger object, so convert it to a boolean
         | 
| 15 23 | 
             
                    @debug.nil? ? false : !!@debug
         | 
| @@ -38,7 +46,7 @@ module Capybara | |
| 38 46 | 
             
                  end
         | 
| 39 47 |  | 
| 40 48 | 
             
                  def mode=(mode)
         | 
| 41 | 
            -
                    @mode = mode
         | 
| 49 | 
            +
                    @mode = mode&.to_sym
         | 
| 42 50 | 
             
                  end
         | 
| 43 51 |  | 
| 44 52 | 
             
                  def enabled=(enabled)
         | 
| @@ -123,43 +123,21 @@ window.CapybaraLockstep = (function() { | |
| 123 123 | 
             
                }
         | 
| 124 124 | 
             
              }
         | 
| 125 125 |  | 
| 126 | 
            -
              function trackInteraction() {
         | 
| 127 | 
            -
                // We already override all interaction methods in the Selenium browser nodes, so they
         | 
| 128 | 
            -
                // wait for an idle frame afterwards. However a test script might also dispatch synthetic
         | 
| 129 | 
            -
                // events with executate_script() to manipulate the browser in ways that are not possible
         | 
| 130 | 
            -
                // with the Capybara API. When we observe such an event we wait until the end of the microtask,
         | 
| 131 | 
            -
                // assuming any busy action will be queued by then.
         | 
| 132 | 
            -
                ['click',  'mousedown', 'keydown', 'change', 'input', 'submit', 'focusin', 'focusout', 'scroll'].forEach(function(eventType) {
         | 
| 133 | 
            -
                  // Use { useCapture: true } so we get the event before another listener
         | 
| 134 | 
            -
                  // can prevent it from bubbling up to the document.
         | 
| 135 | 
            -
                  document.addEventListener(eventType, onInteraction, { capture: true, passive: true })
         | 
| 136 | 
            -
                })
         | 
| 137 | 
            -
              }
         | 
| 138 | 
            -
             | 
| 139 | 
            -
              function onInteraction(event) {
         | 
| 140 | 
            -
                startWork()
         | 
| 141 | 
            -
                // (1) We wait until the end of this microtask, assuming that any callback that
         | 
| 142 | 
            -
                //     would queue an AJAX request or load additional scripts will run by then.
         | 
| 143 | 
            -
                // (2) For performance reasons we don't wait for `waitTasks` here.
         | 
| 144 | 
            -
                //     Whatever was queued by an event handler should call us again, and then
         | 
| 145 | 
            -
                //     we do wait for additional tasks.
         | 
| 146 | 
            -
                Promise.resolve().then(stopWorkNow)
         | 
| 147 | 
            -
              }
         | 
| 148 | 
            -
             | 
| 149 126 | 
             
              function trackRemoteElements() {
         | 
| 150 127 | 
             
                if (!window.MutationObserver) {
         | 
| 151 128 | 
             
                  return
         | 
| 152 129 | 
             
                }
         | 
| 153 130 |  | 
| 154 | 
            -
                // Dynamic imports or analytics snippets may insert a  | 
| 155 | 
            -
                //  | 
| 131 | 
            +
                // Dynamic imports or analytics snippets may insert a script element
         | 
| 132 | 
            +
                // that loads and executes additional JavaScript. We want to be isBusy()
         | 
| 156 133 | 
             
                // until such scripts have loaded or errored.
         | 
| 157 134 | 
             
                let observer = new MutationObserver(onAnyElementChanged)
         | 
| 158 135 | 
             
                observer.observe(document, { subtree: true, childList: true })
         | 
| 159 136 | 
             
              }
         | 
| 160 137 |  | 
| 161 138 | 
             
              function trackJQuery() {
         | 
| 162 | 
            -
                //  | 
| 139 | 
            +
                // CapybaraLockstep.track() is called as the first script in the head.
         | 
| 140 | 
            +
                // jQuery will be loaded after us, so we wait until DOMContentReady.
         | 
| 163 141 | 
             
                whenReady(function() {
         | 
| 164 142 | 
             
                  if (!window.jQuery || waitTasks > 0) {
         | 
| 165 143 | 
             
                    return
         | 
| @@ -180,28 +158,6 @@ window.CapybaraLockstep = (function() { | |
| 180 158 | 
             
                })
         | 
| 181 159 | 
             
              }
         | 
| 182 160 |  | 
| 183 | 
            -
              let INITIALIZING_ATTRIBUTE = 'data-initializing'
         | 
| 184 | 
            -
             | 
| 185 | 
            -
              function trackHydration() {
         | 
| 186 | 
            -
                // Until we have a body on which we can observe [data-initializing]
         | 
| 187 | 
            -
                // we consider ourselves busy.
         | 
| 188 | 
            -
                startWork()
         | 
| 189 | 
            -
                whenReady(function() {
         | 
| 190 | 
            -
                  stopWorkNow()
         | 
| 191 | 
            -
                  if (document.body.hasAttribute(INITIALIZING_ATTRIBUTE)) {
         | 
| 192 | 
            -
                    startWork('Page initialization')
         | 
| 193 | 
            -
                    let observer = new MutationObserver(onInitializingAttributeChanged)
         | 
| 194 | 
            -
                    observer.observe(document.body, { attributes: true, attributeFilter: [INITIALIZING_ATTRIBUTE] })
         | 
| 195 | 
            -
                  }
         | 
| 196 | 
            -
                })
         | 
| 197 | 
            -
              }
         | 
| 198 | 
            -
             | 
| 199 | 
            -
              function onInitializingAttributeChanged() {
         | 
| 200 | 
            -
                if (!document.body.hasAttribute(INITIALIZING_ATTRIBUTE)) {
         | 
| 201 | 
            -
                  stopWork('Page initialization')
         | 
| 202 | 
            -
                }
         | 
| 203 | 
            -
              }
         | 
| 204 | 
            -
             | 
| 205 161 | 
             
              function isRemoteScript(element) {
         | 
| 206 162 | 
             
                if (element.tagName === 'SCRIPT') {
         | 
| 207 163 | 
             
                  let src = element.getAttribute('src')
         | 
| @@ -303,13 +259,28 @@ window.CapybaraLockstep = (function() { | |
| 303 259 | 
             
                }
         | 
| 304 260 | 
             
              }
         | 
| 305 261 |  | 
| 262 | 
            +
              function trackOldUnpoly() {
         | 
| 263 | 
            +
                // CapybaraLockstep.track() is called as the first script in the head.
         | 
| 264 | 
            +
                // Unpoly will be loaded after us, so we wait until DOMContentReady.
         | 
| 265 | 
            +
                whenReady(function() {
         | 
| 266 | 
            +
                  // Unpoly 0.x would wait one task after DOMContentLoaded before booting.
         | 
| 267 | 
            +
                  // There's a slim chance that Capybara can observe the page before compilers have run.
         | 
| 268 | 
            +
                  // Unpoly 1.0+ runs compilers on DOMContentLoaded, so there's no issue.
         | 
| 269 | 
            +
                  if (window.up?.version?.startsWith('0.')) {
         | 
| 270 | 
            +
                    startWork('Old Unpoly')
         | 
| 271 | 
            +
                    setTimeout(function () {
         | 
| 272 | 
            +
                      stopWork('Old Unpoly')
         | 
| 273 | 
            +
                    })
         | 
| 274 | 
            +
                  }
         | 
| 275 | 
            +
                })
         | 
| 276 | 
            +
              }
         | 
| 277 | 
            +
             | 
| 306 278 | 
             
              function track() {
         | 
| 279 | 
            +
                trackOldUnpoly()
         | 
| 307 280 | 
             
                trackFetch()
         | 
| 308 281 | 
             
                trackXHR()
         | 
| 309 | 
            -
                trackInteraction()
         | 
| 310 282 | 
             
                trackRemoteElements()
         | 
| 311 283 | 
             
                trackJQuery()
         | 
| 312 | 
            -
                trackHydration()
         | 
| 313 284 | 
             
              }
         | 
| 314 285 |  | 
| 315 286 | 
             
              function synchronize(callback) {
         | 
| @@ -68,6 +68,8 @@ module Capybara | |
| 68 68 | 
             
                          if (protocol === 'data:' || protocol == 'about:') {
         | 
| 69 69 | 
             
                            done(#{ERROR_PAGE_MISSING.to_json})
         | 
| 70 70 | 
             
                          } else if (document.readyState === 'complete') {
         | 
| 71 | 
            +
                            // WebDriver always waits for the `load` event after a visit(),
         | 
| 72 | 
            +
                            // unless a different page load strategy was configured.
         | 
| 71 73 | 
             
                            synchronize()
         | 
| 72 74 | 
             
                          } else {
         | 
| 73 75 | 
             
                            window.addEventListener('load', synchronize)
         | 
| @@ -88,9 +90,14 @@ module Capybara | |
| 88 90 | 
             
                        end
         | 
| 89 91 | 
             
                      end
         | 
| 90 92 | 
             
                    rescue ::Selenium::WebDriver::Error::ScriptTimeoutError
         | 
| 91 | 
            -
                       | 
| 92 | 
            -
                       | 
| 93 | 
            -
                       | 
| 93 | 
            +
                      timeout_message = "Could not synchronize within #{timeout} seconds"
         | 
| 94 | 
            +
                      log timeout_message
         | 
| 95 | 
            +
                      if timeout_with == :error
         | 
| 96 | 
            +
                        raise Timeout, timeout_message
         | 
| 97 | 
            +
                      else
         | 
| 98 | 
            +
                        # Don't raise an error, this may happen if the server is slow to respond.
         | 
| 99 | 
            +
                        # We will retry on the next Capybara synchronize call.
         | 
| 100 | 
            +
                      end
         | 
| 94 101 | 
             
                    rescue ::Selenium::WebDriver::Error::UnexpectedAlertOpenError
         | 
| 95 102 | 
             
                      log ERROR_ALERT_OPEN
         | 
| 96 103 | 
             
                      # Don't raise an error, this will happen in an innocent test.
         | 
    
        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: 1. | 
| 4 | 
            +
              version: 1.1.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Henning Koch
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: exe
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2021- | 
| 11 | 
            +
            date: 2021-07-08 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: capybara
         |