capybara-lockstep 0.2.3 → 0.3.0

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: aa8ccde381bb125f66222689d93c93008ec2137cfabc664a6ce83ff0701cd7a8
4
- data.tar.gz: 7485b88104f0c4b43387f252e600ad7dc23a917eac28914249b8f279d5ab9df3
3
+ metadata.gz: 6e993a9ec82edc1bbea78694f16ae5b3e171255db9eb7e14064922b153d1838d
4
+ data.tar.gz: 1086e7149ca202b105fd0472c839451845079e05c777a0dba839483fe05c473b
5
5
  SHA512:
6
- metadata.gz: 19f52ab1ca0e9f30fe8be0e791b66bcefe9992d958a104a30c602987e310363587f075f7af19bebf8248e76836a008601c700092bc3b1d53fff3fda5b764dcdc
7
- data.tar.gz: e6380494b6940092842f87e54c898e6a2da9311aa2fd1951a602fb193fb44c7ee6f96907704d80382bd1cc2ccc31a7e0b433e27da8906f889aa6f7178fcd2dff
6
+ metadata.gz: e54e30ae12effa97dcdbd73d0f07b465b90270efc3ddd3ad83fcde7a4d305164ac0380f7ab3128a37870e78862483659eb01ff6bcdf8fade32cd52c3024d5c31
7
+ data.tar.gz: c067bf3f9e0be7d6a84668ee7412ea7d93d450f109e8018940e72392277bcc0f5d0ef92717a1692dee8e0443509bc9317a7b403620b6eb62bdb78539ac036098
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- capybara-lockstep (0.2.3)
4
+ capybara-lockstep (0.3.0)
5
5
  activesupport (>= 3.2)
6
6
  capybara (>= 2.0)
7
7
  selenium-webdriver (>= 3)
data/README.md CHANGED
@@ -220,7 +220,6 @@ ensure
220
220
  end
221
221
  ```
222
222
 
223
-
224
223
  ## Timeout
225
224
 
226
225
  By default capybara-lockstep will wait up to 10 seconds for the page initialize and for JavaScript and AJAX request to finish.
@@ -231,8 +230,28 @@ You can configure a different timeout:
231
230
  Capybara::Lockstep.timeout = 5 # seconds
232
231
  ```
233
232
 
233
+ ## Ruby API
234
+
235
+ capybara-lockstep will automatically patch Capybara to wait for the browser after every command. **This should be enough for most test suites**.
236
+
237
+ For additional edge cases you may interact with capybara-lockstep from your Ruby code.
238
+
239
+
240
+ ### Waiting until the browser is idle
241
+
242
+ This will block until the document was loaded, the DOM has been hydrated and all AJAX requests have concluded:
243
+
244
+ ```ruby
245
+ Capybara::Lockstep.synchronize
246
+ ```
234
247
 
248
+ An example use case is a Cucumber step that explicitely waits for JavaScript to finish, in the rare occasion where capybara-lockstep hasn't picked up an event or request:
235
249
 
250
+ ```gherkin
251
+ When 'I wait for the page to load' do
252
+ Capybara::Lockstep.synchronize
253
+ end
254
+ ```
236
255
 
237
256
  ## JavaScript API
238
257
 
@@ -270,55 +289,29 @@ CapybaraLockstep.isIdle() // => true
270
289
 
271
290
  ### Waiting until the browser is idle
272
291
 
273
- ```js
274
- CapybaraLockstep.awaitIdle(callback)
275
- ```
276
-
277
- ## Ruby API
278
-
279
- capybara-lockstep will automatically patch Capybara to wait for the browser after every command. **This should be enough for most test suites**.
280
-
281
- For additional edge cases you may interact with capybara-lockstep from your Ruby code.
282
-
283
-
284
- ### Waiting until the browser is idle
285
-
286
- This will block until the document was loaded and the DOM has been hydrated:
287
-
288
- ```ruby
289
- Capybara::Lockstep.await_initialized
290
- ```
291
-
292
- This will block while the browser is busy with JavaScript and AJAX requests:
293
-
294
- ```ruby
295
- Capybara::Lockstep.await_idle
296
- ```
297
-
298
- ### Checking if the browser is busy
299
-
300
- You can query capybara-lockstep whether it considers the browser to be busy or idle:
292
+ This will run the given callback once the browser is considered to be idle:
301
293
 
302
- ```ruby
303
- Capybara::Lockstep.idle? # => true
304
- Capybara::Lockstep.busy? # => false
294
+ ```js
295
+ CapybaraLockstep.synchronize(callback)
305
296
  ```
306
297
 
307
-
308
298
  ## Development
309
299
 
310
300
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
311
301
 
312
302
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then 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).
313
303
 
304
+
314
305
  ## Contributing
315
306
 
316
307
  Pull requests are welcome on GitHub at <https://github.com/makandra/capybara-lockstep>.
317
308
 
309
+
318
310
  ## License
319
311
 
320
312
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
321
313
 
314
+
322
315
  ## Credits
323
316
 
324
317
  Henning Koch ([@triskweline](https://twitter.com/triskweline)) from [makandra](https://makandra.com).
@@ -10,8 +10,8 @@ end
10
10
 
11
11
  require_relative 'capybara-lockstep/version'
12
12
  require_relative 'capybara-lockstep/errors'
13
- require_relative 'capybara-lockstep/patiently'
14
13
  require_relative 'capybara-lockstep/configuration'
14
+ require_relative 'capybara-lockstep/logging'
15
15
  require_relative 'capybara-lockstep/lockstep'
16
16
  require_relative 'capybara-lockstep/capybara_ext'
17
17
  require_relative 'capybara-lockstep/helper'
@@ -2,18 +2,23 @@ module Capybara
2
2
  module Lockstep
3
3
  module VisitWithWaiting
4
4
  def visit(*args, &block)
5
- visiting_remote_url = !args[0].start_with?('data:')
5
+ url = args[0]
6
+ # Some of our apps have a Cucumber step that changes drivers mid-scenario.
7
+ # It works by creating a new Capybara session and re-visits the URL from the
8
+ # previous session. If this happens before a URL is ever loaded,
9
+ # it re-visits the URL "data:", which will never "finish" initializing.
10
+ # Also when opening a new tab via Capybara, the initial URL is about:blank.
11
+ visiting_remote_url = !(url.start_with?('data:') || url.start_with?('about:'))
6
12
 
7
- Capybara::Lockstep.catch_up
13
+ if visiting_remote_url
14
+ # We're about to leave this screen, killing all in-flight requests.
15
+ Capybara::Lockstep.synchronize
16
+ end
8
17
 
9
18
  super(*args, &block).tap do
10
- # There is a step that changes drivers mid-scenario.
11
- # It works by creating a new Capybara session and re-visits the
12
- # URL from the previous session. If this happens before a URL is ever
13
- # loaded, it re-visits the URL "data:", which will never "finish"
14
- # initializing.
15
19
  if visiting_remote_url
16
- Capybara::Lockstep.await_initialized
20
+ # puts "After visit: unsynchronizing"
21
+ Capybara::Lockstep.synchronized = false
17
22
  end
18
23
  end
19
24
  end
@@ -28,13 +33,12 @@ end
28
33
 
29
34
  module Capybara
30
35
  module Lockstep
31
- module AwaitIdle
32
- def await_idle(meth)
36
+ module UnsychronizeAfter
37
+ def unsychronize_after(meth)
33
38
  mod = Module.new do
34
39
  define_method meth do |*args, &block|
35
- Capybara::Lockstep.catch_up
36
40
  super(*args, &block).tap do
37
- Capybara::Lockstep.await_idle
41
+ Capybara::Lockstep.synchronized = false
38
42
  end
39
43
  end
40
44
  end
@@ -62,21 +66,21 @@ end
62
66
 
63
67
  node_classes.each do |node_class|
64
68
  node_class.class_eval do
65
- extend Capybara::Lockstep::AwaitIdle
69
+ extend Capybara::Lockstep::UnsychronizeAfter
66
70
 
67
- await_idle :set
68
- await_idle :select_option
69
- await_idle :unselect_option
70
- await_idle :click
71
- await_idle :right_click
72
- await_idle :double_click
73
- await_idle :send_keys
74
- await_idle :hover
75
- await_idle :drag_to
76
- await_idle :drop
77
- await_idle :scroll_by
78
- await_idle :scroll_to
79
- await_idle :trigger
71
+ unsychronize_after :set
72
+ unsychronize_after :select_option
73
+ unsychronize_after :unselect_option
74
+ unsychronize_after :click
75
+ unsychronize_after :right_click
76
+ unsychronize_after :double_click
77
+ unsychronize_after :send_keys
78
+ unsychronize_after :hover
79
+ unsychronize_after :drag_to
80
+ unsychronize_after :drop
81
+ unsychronize_after :scroll_by
82
+ unsychronize_after :scroll_to
83
+ unsychronize_after :trigger
80
84
  end
81
85
  end
82
86
 
@@ -84,7 +88,9 @@ module Capybara
84
88
  module Lockstep
85
89
  module SynchronizeWithCatchUp
86
90
  def synchronize(*args, &block)
87
- Capybara::Lockstep.catch_up
91
+ # This method is called very frequently by capybara.
92
+ # We use the { lazy } option to only synchronize when we're out of sync.
93
+ Capybara::Lockstep.synchronize(lazy: true)
88
94
 
89
95
  super(*args, &block)
90
96
  end
@@ -30,6 +30,10 @@ module Capybara
30
30
  @enabled = enabled
31
31
  end
32
32
 
33
+ def disabled?
34
+ !enabled?
35
+ end
36
+
33
37
  private
34
38
 
35
39
  def javascript_driver?
@@ -37,7 +37,7 @@ window.CapybaraLockstep = (function() {
37
37
 
38
38
  if (isIdle()) {
39
39
  idleCallbacks.forEach(function(callback) {
40
- callback('JavaScript has finished')
40
+ callback('Finished waiting for JavaScript')
41
41
  })
42
42
  idleCallbacks = []
43
43
  }
@@ -116,7 +116,7 @@ window.CapybaraLockstep = (function() {
116
116
  // Dynamic imports or analytics snippets may insert a <script src>
117
117
  // tag that loads and executes additional JavaScript. We want to be isBusy()
118
118
  // until such scripts have loaded or errored.
119
- var observer = new MutationObserver(onMutated)
119
+ var observer = new MutationObserver(onAnyElementChanged)
120
120
  observer.observe(document, { subtree: true, childList: true })
121
121
  }
122
122
 
@@ -139,6 +139,28 @@ window.CapybaraLockstep = (function() {
139
139
  })
140
140
  }
141
141
 
142
+ var INITIALIZING_ATTRIBUTE = 'data-initializing'
143
+
144
+ function trackHydration() {
145
+ // Until we have a body on which we can observe [data-initializing]
146
+ // we consider ourselves busy.
147
+ startWork()
148
+ whenReady(function() {
149
+ stopWork()
150
+ if (document.body.hasAttribute(INITIALIZING_ATTRIBUTE)) {
151
+ startWork()
152
+ var observer = new MutationObserver(onInitializingAttributeChanged)
153
+ observer.observe(document.body, { attributes: true, attributeFilter: [INITIALIZING_ATTRIBUTE] })
154
+ }
155
+ })
156
+ }
157
+
158
+ function onInitializingAttributeChanged() {
159
+ if (!document.body.hasAttribute(INITIALIZING_ATTRIBUTE)) {
160
+ stopWork()
161
+ }
162
+ }
163
+
142
164
  function isRemoteScript(node) {
143
165
  if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'SCRIPT') {
144
166
  var src = node.getAttribute('src')
@@ -155,7 +177,7 @@ window.CapybaraLockstep = (function() {
155
177
  script.addEventListener('error', stopWork)
156
178
  }
157
179
 
158
- function onMutated(changes) {
180
+ function onAnyElementChanged(changes) {
159
181
  changes.forEach(function(change) {
160
182
  change.addedNodes.forEach(function(addedNode) {
161
183
  if (isRemoteScript(addedNode)) {
@@ -168,7 +190,7 @@ window.CapybaraLockstep = (function() {
168
190
  function whenReady(callback) {
169
191
  // Values are "loading", "interactive" and "completed".
170
192
  // https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState
171
- if (document.readyState != 'loading') {
193
+ if (document.readyState !== 'loading') {
172
194
  callback()
173
195
  } else {
174
196
  document.addEventListener('DOMContentLoaded', callback)
@@ -182,9 +204,10 @@ window.CapybaraLockstep = (function() {
182
204
  trackHistory()
183
205
  trackDynamicScripts()
184
206
  trackJQuery()
207
+ trackHydration()
185
208
  }
186
209
 
187
- function awaitIdle(callback) {
210
+ function synchronize(callback) {
188
211
  if (isIdle()) {
189
212
  callback()
190
213
  } else {
@@ -196,7 +219,7 @@ window.CapybaraLockstep = (function() {
196
219
  track: track,
197
220
  startWork: startWork,
198
221
  stopWork: stopWork,
199
- awaitIdle: awaitIdle,
222
+ synchronize: synchronize,
200
223
  isIdle: isIdle,
201
224
  isBusy: isBusy
202
225
  }
@@ -1,133 +1,79 @@
1
1
  module Capybara
2
2
  module Lockstep
3
3
  class << self
4
- include Patiently
5
4
  include Configuration
5
+ include Logging
6
6
 
7
- def await_idle
8
- @delay_await_idle = false
9
- return unless enabled?
10
-
11
- with_max_wait_time(timeout) do
12
- message_from_js = evaluate_async_script(<<~JS)
13
- let done = arguments[0]
14
- if (window.CapybaraLockstep) {
15
- CapybaraLockstep.awaitIdle(done)
16
- } else {
17
- done('Cannot synchronize: Capybara::Lockstep was not included in page')
18
- }
19
- JS
20
- log(message_from_js)
21
- end
22
- rescue ::Selenium::WebDriver::Error::UnexpectedAlertOpenError
23
- log 'Cannot synchronize: Alert is open'
24
- @delay_await_idle = true
25
- end
7
+ attr_accessor :synchronized
26
8
 
27
- def await_initialized
28
- @delay_await_initialized = false
29
- @delay_await_idle = false # since we're also waiting for idle
30
- return unless enabled?
9
+ def synchronized?
10
+ value = page.instance_variable_get(:@lockstep_synchronized)
11
+ # We consider a new Capybara session to be synchronized.
12
+ # This will be set to false after our first visit().
13
+ value.nil? ? true : value
14
+ end
31
15
 
32
- # We're retrying the initialize check every few ms.
33
- # Don't clutter the log with dozens of identical messages.
34
- last_logged_reason = nil
16
+ def synchronized=(value)
17
+ page.instance_variable_set(:@lockstep_synchronized, value)
18
+ end
35
19
 
36
- patiently(timeout) do
37
- if (reason = initialize_reason)
38
- if reason != last_logged_reason
39
- log(reason)
40
- last_logged_reason = reason
41
- end
20
+ ERROR_SNIPPET_MISSING = 'Cannot synchronize: Capybara::Lockstep JavaScript snippet is missing on page'
21
+ ERROR_PAGE_MISSING = 'Cannot synchronize before initial Capybara visit'
42
22
 
43
- # Raise an exception that will be retried by `patiently`
44
- raise Busy, reason
45
- end
23
+ def synchronize(lazy: false)
24
+ if (lazy && synchronized?) || @synchronizing || disabled?
25
+ return
46
26
  end
47
- rescue ::Selenium::WebDriver::Error::UnexpectedAlertOpenError
48
- log 'Cannot synchronize: Alert is open'
49
- @delay_await_initialized = true
50
- end
51
27
 
52
- def catch_up
53
- return if @catching_up
28
+ @synchronizing = true
29
+
30
+ log 'Synchronizing'
54
31
 
55
32
  begin
56
- @catching_up = true
57
- if @delay_await_initialized
58
- log 'Retrying synchronization'
59
- await_initialized
60
- # elsif browser_made_full_page_load?
61
- # log 'Browser loaded new page'
62
- # await_initialized
63
- elsif @delay_await_idle
64
- log 'Retrying synchronization'
65
- await_idle
33
+ with_max_wait_time(timeout) do
34
+ message_from_js = evaluate_async_script(<<~JS)
35
+ let done = arguments[0]
36
+ let synchronize = () => {
37
+ if (window.CapybaraLockstep) {
38
+ CapybaraLockstep.synchronize(done)
39
+ } else {
40
+ done(#{ERROR_SNIPPET_MISSING.to_json})
41
+ }
42
+ }
43
+ let protocol = location.protocol
44
+ if (protocol === 'data:' || protocol == 'about:') {
45
+ done(#{ERROR_PAGE_MISSING.to_json})
46
+ } else if (document.readyState === 'complete') {
47
+ synchronize()
48
+ } else {
49
+ window.addEventListener('load', synchronize)
50
+ }
51
+ JS
52
+
53
+ case message_from_js
54
+ when ERROR_PAGE_MISSING
55
+ log(message_from_js)
56
+ self.synchronized = false
57
+ when ERROR_SNIPPET_MISSING
58
+ log(message_from_js)
59
+ self.synchronized = false
60
+ else
61
+ log message_from_js
62
+ log "Synchronized sucessfully"
63
+ self.synchronized = true
64
+ end
66
65
  end
66
+ rescue StandardError => e
67
+ log "#{e.class.name} while synchronizing: #{e.message}"
68
+ @synchronized = false
69
+ raise e
67
70
  ensure
68
- @catching_up = false
71
+ @synchronizing = false
69
72
  end
70
73
  end
71
74
 
72
- def idle?
73
- unless enabled?
74
- return true
75
- end
76
-
77
- result = execute_script(<<~JS)
78
- if (window.CapybaraLockstep) {
79
- return CapybaraLockstep.isIdle()
80
- } else {
81
- return 'Cannot check busy state: Capybara::Lockstep was not included in page'
82
- }
83
- JS
84
-
85
- if result.is_a?(String)
86
- log(result)
87
- # When the snippet is missing we assume that the browser is idle.
88
- # Otherwise we would wait forever.
89
- true
90
- else
91
- result
92
- end
93
- end
94
-
95
- def busy?
96
- !idle?
97
- end
98
-
99
75
  private
100
76
 
101
- def browser_made_full_page_load?
102
- # Page change without visit()
103
- page.has_css?('body[data-hydrating]')
104
- end
105
-
106
- def initialize_reason
107
- execute_script(<<~JS)
108
- if (location.href.indexOf('data:') == 0) {
109
- return 'Requesting initial page'
110
- }
111
-
112
- if (document.readyState !== "complete") {
113
- return 'Document is loading'
114
- }
115
-
116
- // The application layouts render a <body data-initializing>.
117
- // The [data-initializing] attribute is removed by an Angular directive or Unpoly compiler (frontend).
118
- // to signal that all elements have been activated.
119
- if (document.querySelector('body[data-initializing]')) {
120
- return 'DOM is being hydrated'
121
- }
122
-
123
- if (window.CapybaraLockstep && CapybaraLockstep.isBusy()) {
124
- return 'JavaScript or AJAX requests are running'
125
- }
126
-
127
- return false
128
- JS
129
- end
130
-
131
77
  def page
132
78
  Capybara.current_session
133
79
  end
@@ -144,17 +90,10 @@ module Capybara
144
90
  end
145
91
  end
146
92
 
147
- def log(message)
148
- if debug? && message.present?
149
- message = "[Capybara::Lockstep] #{message}"
150
- if @debug.respond_to?(:debug)
151
- # If someone set Capybara::Lockstep to a logger, use that
152
- @debug.debug(message)
153
- else
154
- # Otherwise print to STDOUT
155
- puts message
156
- end
157
- end
93
+ def ignoring_alerts(&block)
94
+ block.call
95
+ rescue ::Selenium::WebDriver::Error::UnexpectedAlertOpenError
96
+ # no-op
158
97
  end
159
98
 
160
99
  end
@@ -0,0 +1,18 @@
1
+ module Capybara
2
+ module Lockstep
3
+ module Logging
4
+ def log(message)
5
+ if debug? && message.present?
6
+ message = "[Capybara::Lockstep] #{message}"
7
+ if @debug.respond_to?(:debug)
8
+ # If someone set Capybara::Lockstep to a logger, use that
9
+ @debug.debug(message)
10
+ else
11
+ # Otherwise print to STDOUT
12
+ puts message
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,5 +1,5 @@
1
1
  module Capybara
2
2
  module Lockstep
3
- VERSION = "0.2.3"
3
+ VERSION = "0.3.0"
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: 0.2.3
4
+ version: 0.3.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-03-03 00:00:00.000000000 Z
11
+ date: 2021-03-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: capybara
@@ -77,7 +77,7 @@ files:
77
77
  - lib/capybara-lockstep/helper.js
78
78
  - lib/capybara-lockstep/helper.rb
79
79
  - lib/capybara-lockstep/lockstep.rb
80
- - lib/capybara-lockstep/patiently.rb
80
+ - lib/capybara-lockstep/logging.rb
81
81
  - lib/capybara-lockstep/version.rb
82
82
  homepage: https://github.com/makandra/capybara-lockstep
83
83
  licenses:
@@ -1,58 +0,0 @@
1
- module Capybara
2
- module Lockstep
3
- # Ported from https://github.com/makandra/spreewald/blob/master/lib/spreewald_support/tolerance_for_selenium_sync_issues.rb
4
- module Patiently
5
-
6
- RETRY_ERRORS = %w[
7
- Capybara::Lockstep::Busy
8
- Capybara::ElementNotFound
9
- Spec::Expectations::ExpectationNotMetError
10
- RSpec::Expectations::ExpectationNotMetError
11
- Minitest::Assertion
12
- Capybara::Poltergeist::ClickFailed
13
- Capybara::ExpectationNotMet
14
- Selenium::WebDriver::Error::StaleElementReferenceError
15
- Selenium::WebDriver::Error::NoAlertPresentError
16
- Selenium::WebDriver::Error::ElementNotVisibleError
17
- Selenium::WebDriver::Error::NoSuchFrameError
18
- Selenium::WebDriver::Error::NoAlertPresentError
19
- Selenium::WebDriver::Error::JavascriptError
20
- Selenium::WebDriver::Error::UnknownError
21
- Selenium::WebDriver::Error::NoSuchAlertError
22
- ]
23
-
24
- # evaluate_script latency is ~ 0.025s
25
- WAIT_PERIOD = 0.03
26
-
27
- def patiently(timeout = Capybara.default_max_wait_time, &block)
28
- started = monotonic_time
29
- tries = 0
30
- begin
31
- tries += 1
32
- block.call
33
- rescue Exception => e
34
- raise e unless retryable_error?(e)
35
- raise e if (monotonic_time - started > timeout && tries >= 2)
36
- sleep(WAIT_PERIOD)
37
- if monotonic_time == started
38
- raise Capybara::FrozenInTime, "time appears to be frozen, Capybara does not work with libraries which freeze time, consider using time travelling instead"
39
- end
40
- retry
41
- end
42
- end
43
-
44
- private
45
-
46
- def monotonic_time
47
- # We use the system clock (i.e. seconds since boot) to calculate the time,
48
- # because Time.now may be frozen
49
- Process.clock_gettime(Process::CLOCK_MONOTONIC)
50
- end
51
-
52
- def retryable_error?(e)
53
- RETRY_ERRORS.include?(e.class.name)
54
- end
55
-
56
- end
57
- end
58
- end