turbo-rails 2.0.7 → 2.0.9
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/README.md +59 -0
- data/app/assets/javascripts/turbo.js +1991 -1914
- data/app/assets/javascripts/turbo.min.js +6 -6
- data/app/assets/javascripts/turbo.min.js.map +1 -1
- data/app/javascript/turbo/cable_stream_source_element.js +10 -0
- data/app/models/concerns/turbo/broadcastable.rb +32 -22
- data/app/models/turbo/streams/tag_builder.rb +30 -12
- data/lib/turbo/engine.rb +48 -3
- data/lib/turbo/system_test_helper.rb +128 -0
- data/lib/turbo/version.rb +1 -1
- metadata +3 -16
@@ -3,6 +3,8 @@ import { subscribeTo } from "./cable"
|
|
3
3
|
import snakeize from "./snakeize"
|
4
4
|
|
5
5
|
class TurboCableStreamSourceElement extends HTMLElement {
|
6
|
+
static observedAttributes = ["channel", "signed-stream-name"]
|
7
|
+
|
6
8
|
async connectedCallback() {
|
7
9
|
connectStreamSource(this)
|
8
10
|
this.subscription = await subscribeTo(this.channel, {
|
@@ -15,6 +17,14 @@ class TurboCableStreamSourceElement extends HTMLElement {
|
|
15
17
|
disconnectedCallback() {
|
16
18
|
disconnectStreamSource(this)
|
17
19
|
if (this.subscription) this.subscription.unsubscribe()
|
20
|
+
this.subscriptionDisconnected()
|
21
|
+
}
|
22
|
+
|
23
|
+
attributeChangedCallback() {
|
24
|
+
if (this.subscription) {
|
25
|
+
this.disconnectedCallback()
|
26
|
+
this.connectedCallback()
|
27
|
+
}
|
18
28
|
}
|
19
29
|
|
20
30
|
dispatchMessageEvent(data) {
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Turbo streams can be broadcasted directly from models that include this module (this is automatically done for Active Records).
|
1
|
+
# Turbo streams can be broadcasted directly from models that include this module (this is automatically done for Active Records if ActiveJob is loaded).
|
2
2
|
# This makes it convenient to execute both synchronous and asynchronous updates, and render directly from callbacks in models
|
3
3
|
# or from controllers or jobs that act on those models. Here's an example:
|
4
4
|
#
|
@@ -241,13 +241,13 @@ module Turbo::Broadcastable
|
|
241
241
|
#
|
242
242
|
# # Sends <turbo-stream action="remove" target="clearance_5"></turbo-stream> to the stream named "identity:2:clearances"
|
243
243
|
# clearance.broadcast_remove_to examiner.identity, :clearances
|
244
|
-
def broadcast_remove_to(*streamables, target: self)
|
245
|
-
Turbo::StreamsChannel.broadcast_remove_to(*streamables, target: target) unless suppressed_turbo_broadcasts?
|
244
|
+
def broadcast_remove_to(*streamables, target: self, **rendering)
|
245
|
+
Turbo::StreamsChannel.broadcast_remove_to(*streamables, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts?
|
246
246
|
end
|
247
247
|
|
248
248
|
# Same as <tt>#broadcast_remove_to</tt>, but the designated stream is automatically set to the current model.
|
249
|
-
def broadcast_remove
|
250
|
-
broadcast_remove_to self
|
249
|
+
def broadcast_remove(**rendering)
|
250
|
+
broadcast_remove_to self, **rendering
|
251
251
|
end
|
252
252
|
|
253
253
|
# Replace this broadcastable model in the dom for subscribers of the stream name identified by the passed
|
@@ -265,7 +265,7 @@ module Turbo::Broadcastable
|
|
265
265
|
# # to the stream named "identity:2:clearances"
|
266
266
|
# clearance.broadcast_replace_to examiner.identity, :clearance, attributes: { method: :morph }, partial: "clearances/other_partial", locals: { a: 1 }
|
267
267
|
def broadcast_replace_to(*streamables, **rendering)
|
268
|
-
Turbo::StreamsChannel.broadcast_replace_to(*streamables, target: self
|
268
|
+
Turbo::StreamsChannel.broadcast_replace_to(*streamables, **extract_options_and_add_target(rendering, target: self)) unless suppressed_turbo_broadcasts?
|
269
269
|
end
|
270
270
|
|
271
271
|
# Same as <tt>#broadcast_replace_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -288,7 +288,7 @@ module Turbo::Broadcastable
|
|
288
288
|
# # to the stream named "identity:2:clearances"
|
289
289
|
# # clearance.broadcast_update_to examiner.identity, :clearances, attributes: { method: :morph }, partial: "clearances/other_partial", locals: { a: 1 }
|
290
290
|
def broadcast_update_to(*streamables, **rendering)
|
291
|
-
Turbo::StreamsChannel.broadcast_update_to(*streamables, target: self
|
291
|
+
Turbo::StreamsChannel.broadcast_update_to(*streamables, **extract_options_and_add_target(rendering, target: self)) unless suppressed_turbo_broadcasts?
|
292
292
|
end
|
293
293
|
|
294
294
|
# Same as <tt>#broadcast_update_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -308,8 +308,10 @@ module Turbo::Broadcastable
|
|
308
308
|
# # to the stream named "identity:2:clearances"
|
309
309
|
# clearance.broadcast_before_to examiner.identity, :clearances, target: "clearance_5",
|
310
310
|
# partial: "clearances/other_partial", locals: { a: 1 }
|
311
|
-
def broadcast_before_to(*streamables, target
|
312
|
-
|
311
|
+
def broadcast_before_to(*streamables, target: nil, targets: nil, **rendering)
|
312
|
+
raise ArgumentError, "at least one of target or targets is required" unless target || targets
|
313
|
+
|
314
|
+
Turbo::StreamsChannel.broadcast_before_to(*streamables, **extract_options_and_add_target(rendering.merge(target: target, targets: targets)))
|
313
315
|
end
|
314
316
|
|
315
317
|
# Insert a rendering of this broadcastable model after the target identified by it's dom id passed as <tt>target</tt>
|
@@ -324,8 +326,10 @@ module Turbo::Broadcastable
|
|
324
326
|
# # to the stream named "identity:2:clearances"
|
325
327
|
# clearance.broadcast_after_to examiner.identity, :clearances, target: "clearance_5",
|
326
328
|
# partial: "clearances/other_partial", locals: { a: 1 }
|
327
|
-
def broadcast_after_to(*streamables, target
|
328
|
-
|
329
|
+
def broadcast_after_to(*streamables, target: nil, targets: nil, **rendering)
|
330
|
+
raise ArgumentError, "at least one of target or targets is required" unless target || targets
|
331
|
+
|
332
|
+
Turbo::StreamsChannel.broadcast_after_to(*streamables, **extract_options_and_add_target(rendering.merge(target: target, targets: targets)))
|
329
333
|
end
|
330
334
|
|
331
335
|
# Append a rendering of this broadcastable model to the target identified by it's dom id passed as <tt>target</tt>
|
@@ -341,7 +345,7 @@ module Turbo::Broadcastable
|
|
341
345
|
# clearance.broadcast_append_to examiner.identity, :clearances, target: "clearances",
|
342
346
|
# partial: "clearances/other_partial", locals: { a: 1 }
|
343
347
|
def broadcast_append_to(*streamables, target: broadcast_target_default, **rendering)
|
344
|
-
Turbo::StreamsChannel.broadcast_append_to(*streamables, target: target
|
348
|
+
Turbo::StreamsChannel.broadcast_append_to(*streamables, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts?
|
345
349
|
end
|
346
350
|
|
347
351
|
# Same as <tt>#broadcast_append_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -362,7 +366,7 @@ module Turbo::Broadcastable
|
|
362
366
|
# clearance.broadcast_prepend_to examiner.identity, :clearances, target: "clearances",
|
363
367
|
# partial: "clearances/other_partial", locals: { a: 1 }
|
364
368
|
def broadcast_prepend_to(*streamables, target: broadcast_target_default, **rendering)
|
365
|
-
Turbo::StreamsChannel.broadcast_prepend_to(*streamables, target: target
|
369
|
+
Turbo::StreamsChannel.broadcast_prepend_to(*streamables, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts?
|
366
370
|
end
|
367
371
|
|
368
372
|
# Same as <tt>#broadcast_prepend_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -389,7 +393,7 @@ module Turbo::Broadcastable
|
|
389
393
|
# # to the stream named "identity:2:clearances"
|
390
394
|
# clearance.broadcast_action_to examiner.identity, :clearances, action: :prepend, target: "clearances"
|
391
395
|
def broadcast_action_to(*streamables, action:, target: broadcast_target_default, attributes: {}, **rendering)
|
392
|
-
Turbo::StreamsChannel.broadcast_action_to(*streamables, action: action,
|
396
|
+
Turbo::StreamsChannel.broadcast_action_to(*streamables, action: action, attributes: attributes, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts?
|
393
397
|
end
|
394
398
|
|
395
399
|
# Same as <tt>#broadcast_action_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -399,7 +403,7 @@ module Turbo::Broadcastable
|
|
399
403
|
|
400
404
|
# Same as <tt>broadcast_replace_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
401
405
|
def broadcast_replace_later_to(*streamables, **rendering)
|
402
|
-
Turbo::StreamsChannel.broadcast_replace_later_to(*streamables, target: self
|
406
|
+
Turbo::StreamsChannel.broadcast_replace_later_to(*streamables, **extract_options_and_add_target(rendering, target: self)) unless suppressed_turbo_broadcasts?
|
403
407
|
end
|
404
408
|
|
405
409
|
# Same as <tt>#broadcast_replace_later_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -409,7 +413,7 @@ module Turbo::Broadcastable
|
|
409
413
|
|
410
414
|
# Same as <tt>broadcast_update_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
411
415
|
def broadcast_update_later_to(*streamables, **rendering)
|
412
|
-
Turbo::StreamsChannel.broadcast_update_later_to(*streamables, target: self
|
416
|
+
Turbo::StreamsChannel.broadcast_update_later_to(*streamables, **extract_options_and_add_target(rendering, target: self)) unless suppressed_turbo_broadcasts?
|
413
417
|
end
|
414
418
|
|
415
419
|
# Same as <tt>#broadcast_update_later_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -419,7 +423,7 @@ module Turbo::Broadcastable
|
|
419
423
|
|
420
424
|
# Same as <tt>broadcast_append_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
421
425
|
def broadcast_append_later_to(*streamables, target: broadcast_target_default, **rendering)
|
422
|
-
Turbo::StreamsChannel.broadcast_append_later_to(*streamables, target: target
|
426
|
+
Turbo::StreamsChannel.broadcast_append_later_to(*streamables, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts?
|
423
427
|
end
|
424
428
|
|
425
429
|
# Same as <tt>#broadcast_append_later_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -429,7 +433,7 @@ module Turbo::Broadcastable
|
|
429
433
|
|
430
434
|
# Same as <tt>broadcast_prepend_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
431
435
|
def broadcast_prepend_later_to(*streamables, target: broadcast_target_default, **rendering)
|
432
|
-
Turbo::StreamsChannel.broadcast_prepend_later_to(*streamables, target: target
|
436
|
+
Turbo::StreamsChannel.broadcast_prepend_later_to(*streamables, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts?
|
433
437
|
end
|
434
438
|
|
435
439
|
# Same as <tt>#broadcast_prepend_later_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -449,7 +453,7 @@ module Turbo::Broadcastable
|
|
449
453
|
|
450
454
|
# Same as <tt>broadcast_action_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
451
455
|
def broadcast_action_later_to(*streamables, action:, target: broadcast_target_default, attributes: {}, **rendering)
|
452
|
-
Turbo::StreamsChannel.broadcast_action_later_to(*streamables, action: action,
|
456
|
+
Turbo::StreamsChannel.broadcast_action_later_to(*streamables, action: action, attributes: attributes, **extract_options_and_add_target(rendering, target: target)) unless suppressed_turbo_broadcasts?
|
453
457
|
end
|
454
458
|
|
455
459
|
# Same as <tt>#broadcast_action_later_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -485,7 +489,7 @@ module Turbo::Broadcastable
|
|
485
489
|
# desireable for model callbacks, certainly not if those callbacks are inside of a transaction. Most of the time you should
|
486
490
|
# be using +broadcast_render_later_to+, unless you specifically know why synchronous rendering is needed.
|
487
491
|
def broadcast_render_to(*streamables, **rendering)
|
488
|
-
Turbo::StreamsChannel.broadcast_render_to(*streamables, **
|
492
|
+
Turbo::StreamsChannel.broadcast_render_to(*streamables, **extract_options_and_add_target(rendering, target: self)) unless suppressed_turbo_broadcasts?
|
489
493
|
end
|
490
494
|
|
491
495
|
# Same as <tt>broadcast_render_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
@@ -496,7 +500,7 @@ module Turbo::Broadcastable
|
|
496
500
|
# Same as <tt>broadcast_render_later</tt> but run with the added option of naming the stream using the passed
|
497
501
|
# <tt>streamables</tt>.
|
498
502
|
def broadcast_render_later_to(*streamables, **rendering)
|
499
|
-
Turbo::StreamsChannel.broadcast_render_later_to(*streamables, **
|
503
|
+
Turbo::StreamsChannel.broadcast_render_later_to(*streamables, **extract_options_and_add_target(rendering)) unless suppressed_turbo_broadcasts?
|
500
504
|
end
|
501
505
|
|
502
506
|
private
|
@@ -504,11 +508,17 @@ module Turbo::Broadcastable
|
|
504
508
|
self.class.broadcast_target_default
|
505
509
|
end
|
506
510
|
|
511
|
+
def extract_options_and_add_target(rendering = {}, target: broadcast_target_default)
|
512
|
+
broadcast_rendering_with_defaults(rendering).tap do |options|
|
513
|
+
options[:target] = target if !options.key?(:target) && !options.key?(:targets)
|
514
|
+
end
|
515
|
+
end
|
516
|
+
|
507
517
|
def broadcast_rendering_with_defaults(options)
|
508
518
|
options.tap do |o|
|
509
519
|
# Add the current instance into the locals with the element name (which is the un-namespaced name)
|
510
520
|
# as the key. This parallels how the ActionView::ObjectRenderer would create a local variable.
|
511
|
-
o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.element.to_sym => self
|
521
|
+
o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.element.to_sym => self).compact
|
512
522
|
|
513
523
|
if o[:html] || o[:partial]
|
514
524
|
return o
|
@@ -77,8 +77,9 @@ class Turbo::Streams::TagBuilder
|
|
77
77
|
# <%= turbo_stream.replace "clearance_5" do %>
|
78
78
|
# <div id='clearance_5'>Replace the dom target identified by clearance_5</div>
|
79
79
|
# <% end %>
|
80
|
-
|
81
|
-
|
80
|
+
# <%= turbo_stream.replace clearance, "<div>Morph the dom target</div>", method: :morph %>
|
81
|
+
def replace(target, content = nil, method: nil, **rendering, &block)
|
82
|
+
action :replace, target, content, method: method, **rendering, &block
|
82
83
|
end
|
83
84
|
|
84
85
|
# Replace the <tt>targets</tt> in the dom with either the <tt>content</tt> passed in, a rendering result determined
|
@@ -90,8 +91,9 @@ class Turbo::Streams::TagBuilder
|
|
90
91
|
# <%= turbo_stream.replace_all ".clearance_item" do %>
|
91
92
|
# <div class='.clearance_item'>Replace the dom target identified by the class clearance_item</div>
|
92
93
|
# <% end %>
|
93
|
-
|
94
|
-
|
94
|
+
# <%= turbo_stream.replace_all clearance, "<div>Morph the dom target</div>", method: :morph %>
|
95
|
+
def replace_all(targets, content = nil, method: nil, **rendering, &block)
|
96
|
+
action_all :replace, targets, content, method: method, **rendering, &block
|
95
97
|
end
|
96
98
|
|
97
99
|
# Insert the <tt>content</tt> passed in, a rendering result determined by the <tt>rendering</tt> keyword arguments,
|
@@ -155,8 +157,9 @@ class Turbo::Streams::TagBuilder
|
|
155
157
|
# <%= turbo_stream.update "clearance_5" do %>
|
156
158
|
# Update the content of the dom target identified by clearance_5
|
157
159
|
# <% end %>
|
158
|
-
|
159
|
-
|
160
|
+
# <%= turbo_stream.update clearance, "<div>Morph the dom target</div>", method: :morph %>
|
161
|
+
def update(target, content = nil, method: nil, **rendering, &block)
|
162
|
+
action :update, target, content, method: method, **rendering, &block
|
160
163
|
end
|
161
164
|
|
162
165
|
# Update the <tt>targets</tt> in the dom with either the <tt>content</tt> passed in or a rendering result determined
|
@@ -168,8 +171,9 @@ class Turbo::Streams::TagBuilder
|
|
168
171
|
# <%= turbo_stream.update_all "clearance_item" do %>
|
169
172
|
# Update the content of the dom target identified by the class clearance_item
|
170
173
|
# <% end %>
|
171
|
-
|
172
|
-
|
174
|
+
# <%= turbo_stream.update_all clearance, "<div>Morph the dom target</div>", method: :morph %>
|
175
|
+
def update_all(targets, content = nil, method: nil, **rendering, &block)
|
176
|
+
action_all :update, targets, content, method: method, **rendering, &block
|
173
177
|
end
|
174
178
|
|
175
179
|
# Append to the target in the dom identified with <tt>target</tt> either the <tt>content</tt> passed in or a
|
@@ -228,23 +232,37 @@ class Turbo::Streams::TagBuilder
|
|
228
232
|
action_all :prepend, targets, content, **rendering, &block
|
229
233
|
end
|
230
234
|
|
235
|
+
# Creates a `turbo-stream` tag with an `[action="refresh"`] attribute and a
|
236
|
+
# `[request-id]` attribute that defaults to `Turbo.current_request_id`:
|
237
|
+
#
|
238
|
+
# turbo_stream.refresh
|
239
|
+
# # => <turbo-stream action="refresh" request-id="ef083d55-7516-41b1-ad28-16f553399c6a"></turbo-stream>
|
240
|
+
#
|
241
|
+
# turbo_stream.refresh request_id: "abc123"
|
242
|
+
# # => <turbo-stream action="refresh" request-id="abc123"></turbo-stream>
|
243
|
+
def refresh(...)
|
244
|
+
turbo_stream_refresh_tag(...)
|
245
|
+
end
|
246
|
+
|
231
247
|
# Send an action of the type <tt>name</tt> to <tt>target</tt>. Options described in the concrete methods.
|
232
|
-
def action(name, target, content = nil, allow_inferred_rendering: true, **rendering, &block)
|
248
|
+
def action(name, target, content = nil, method: nil, allow_inferred_rendering: true, **rendering, &block)
|
233
249
|
template = render_template(target, content, allow_inferred_rendering: allow_inferred_rendering, **rendering, &block)
|
234
250
|
|
235
|
-
turbo_stream_action_tag name, target: target, template: template
|
251
|
+
turbo_stream_action_tag name, target: target, template: template, method: method
|
236
252
|
end
|
237
253
|
|
238
254
|
# Send an action of the type <tt>name</tt> to <tt>targets</tt>. Options described in the concrete methods.
|
239
|
-
def action_all(name, targets, content = nil, allow_inferred_rendering: true, **rendering, &block)
|
255
|
+
def action_all(name, targets, content = nil, method: nil, allow_inferred_rendering: true, **rendering, &block)
|
240
256
|
template = render_template(targets, content, allow_inferred_rendering: allow_inferred_rendering, **rendering, &block)
|
241
257
|
|
242
|
-
turbo_stream_action_tag name, targets: targets, template: template
|
258
|
+
turbo_stream_action_tag name, targets: targets, template: template, method: method
|
243
259
|
end
|
244
260
|
|
245
261
|
private
|
246
262
|
def render_template(target, content = nil, allow_inferred_rendering: true, **rendering, &block)
|
247
263
|
case
|
264
|
+
when target.respond_to?(:render_in) && content.nil?
|
265
|
+
target.render_in(@view_context, &block)
|
248
266
|
when content.respond_to?(:render_in)
|
249
267
|
content.render_in(@view_context, &block)
|
250
268
|
when content
|
data/lib/turbo/engine.rb
CHANGED
@@ -5,6 +5,7 @@ module Turbo
|
|
5
5
|
isolate_namespace Turbo
|
6
6
|
config.eager_load_namespaces << Turbo
|
7
7
|
config.turbo = ActiveSupport::OrderedOptions.new
|
8
|
+
config.turbo.test_connect_after_actions = %i[visit]
|
8
9
|
config.autoload_once_paths = %W(
|
9
10
|
#{root}/app/channels
|
10
11
|
#{root}/app/controllers
|
@@ -15,6 +16,27 @@ module Turbo
|
|
15
16
|
#{root}/app/jobs
|
16
17
|
)
|
17
18
|
|
19
|
+
# If the parent application does not use Active Job, app/jobs cannot
|
20
|
+
# be eager loaded, because it references the ActiveJob constant.
|
21
|
+
#
|
22
|
+
# When turbo-rails depends on Rails 7 or above, the entire block can be
|
23
|
+
# reduced to
|
24
|
+
#
|
25
|
+
# unless defined?(ActiveJob)
|
26
|
+
# Rails.autoloaders.once.do_not_eager_load("#{root}/app/jobs")
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
initializer "turbo.no_active_job", before: :set_eager_load_paths do
|
30
|
+
unless defined?(ActiveJob)
|
31
|
+
if Rails.autoloaders.zeitwerk_enabled?
|
32
|
+
Rails.autoloaders.once.do_not_eager_load("#{root}/app/jobs")
|
33
|
+
else
|
34
|
+
# This else branch only runs in Rails 6.x + classic mode.
|
35
|
+
config.eager_load_paths.delete("#{root}/app/jobs")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
18
40
|
# If the parent application does not use Action Cable, app/channels cannot
|
19
41
|
# be eager loaded, because it references the ActionCable constant.
|
20
42
|
#
|
@@ -71,7 +93,9 @@ module Turbo
|
|
71
93
|
|
72
94
|
initializer "turbo.broadcastable" do
|
73
95
|
ActiveSupport.on_load(:active_record) do
|
74
|
-
|
96
|
+
if defined?(ActiveJob)
|
97
|
+
include Turbo::Broadcastable
|
98
|
+
end
|
75
99
|
end
|
76
100
|
end
|
77
101
|
|
@@ -101,8 +125,10 @@ module Turbo
|
|
101
125
|
|
102
126
|
ActiveSupport.on_load(:action_cable) do
|
103
127
|
ActiveSupport.on_load(:active_support_test_case) do
|
104
|
-
|
105
|
-
|
128
|
+
if defined?(ActiveJob)
|
129
|
+
require "turbo/broadcastable/test_helper"
|
130
|
+
include Turbo::Broadcastable::TestHelper
|
131
|
+
end
|
106
132
|
end
|
107
133
|
end
|
108
134
|
|
@@ -126,5 +152,24 @@ module Turbo
|
|
126
152
|
end
|
127
153
|
end
|
128
154
|
end
|
155
|
+
|
156
|
+
initializer "turbo.system_test_helper" do
|
157
|
+
ActiveSupport.on_load(:action_dispatch_system_test_case) do
|
158
|
+
require "turbo/system_test_helper"
|
159
|
+
include Turbo::SystemTestHelper
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
config.after_initialize do |app|
|
164
|
+
ActiveSupport.on_load(:action_dispatch_system_test_case) do
|
165
|
+
app.config.turbo.test_connect_after_actions.map do |method|
|
166
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
167
|
+
def #{method}(...) # def visit(...)
|
168
|
+
super.tap { connect_turbo_cable_stream_sources } # super.tap { connect_turbo_cable_stream_sources }
|
169
|
+
end # end
|
170
|
+
RUBY
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
129
174
|
end
|
130
175
|
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
module Turbo::SystemTestHelper
|
2
|
+
# Delay until every `<turbo-cable-stream-source>` element present in the page
|
3
|
+
# is ready to receive broadcasts
|
4
|
+
#
|
5
|
+
# test "renders broadcasted Messages" do
|
6
|
+
# message = Message.new content: "Hello, from Action Cable"
|
7
|
+
#
|
8
|
+
# visit "/"
|
9
|
+
# click_link "All Messages"
|
10
|
+
# message.save! # execute server-side code to broadcast a Message
|
11
|
+
#
|
12
|
+
# assert_text message.content
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# By default, calls to `#visit` will wait for all `<turbo-cable-stream-source>`
|
16
|
+
# elements to connect. You can control this by modifying the
|
17
|
+
# `config.turbo.test_connect_after_actions`. For example, to wait after calls to
|
18
|
+
# `#click_link`, add the following to `config/environments/test.rb`:
|
19
|
+
#
|
20
|
+
# # config/environments/test.rb
|
21
|
+
# config.turbo.test_connect_after_actions << :click_link
|
22
|
+
#
|
23
|
+
# To disable automatic connecting, set the configuration to `[]`:
|
24
|
+
#
|
25
|
+
# # config/environments/test.rb
|
26
|
+
# config.turbo.test_connect_after_actions = []
|
27
|
+
#
|
28
|
+
def connect_turbo_cable_stream_sources(**options, &block)
|
29
|
+
all(:turbo_cable_stream_source, **options, connected: false, wait: 0).each do |element|
|
30
|
+
element.assert_matches_selector(:turbo_cable_stream_source, **options, connected: true, &block)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Asserts that a `<turbo-cable-stream-source>` element is present in the
|
35
|
+
# document
|
36
|
+
#
|
37
|
+
# ==== Arguments
|
38
|
+
#
|
39
|
+
# * <tt>locator</tt> optional locator to determine the element's
|
40
|
+
# `[signed-stream-name]` attribute. Can be of any type that is a valid
|
41
|
+
# argument to <tt>Turbo::Streams::StreamName#signed_stream_name</tt>.
|
42
|
+
#
|
43
|
+
# ==== Options
|
44
|
+
#
|
45
|
+
# * <tt>:connected</tt> matches the `[connected]` attribute
|
46
|
+
# * <tt>:channel</tt> matches the `[channel]` attribute. Can be a Class,
|
47
|
+
# String, Symbol, or Regexp
|
48
|
+
# * <tt>:signed_stream_name</tt> matches the element's `[signed-stream-name]`
|
49
|
+
# attribute. Can be of any type that is a valid
|
50
|
+
# argument to <tt>Turbo::Streams::StreamName#signed_stream_name</tt>.
|
51
|
+
#
|
52
|
+
# In addition to the filters listed above, accepts any valid Capybara global
|
53
|
+
# filter option.
|
54
|
+
def assert_turbo_cable_stream_source(...)
|
55
|
+
assert_selector(:turbo_cable_stream_source, ...)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Asserts that a `<turbo-cable-stream-source>` element is absent from the
|
59
|
+
# document
|
60
|
+
#
|
61
|
+
# ==== Arguments
|
62
|
+
#
|
63
|
+
# * <tt>locator</tt> optional locator to determine the element's
|
64
|
+
# `[signed-stream-name]` attribute. Can be of any type that is a valid
|
65
|
+
# argument to <tt>Turbo::Streams::StreamName#signed_stream_name</tt>.
|
66
|
+
#
|
67
|
+
# ==== Options
|
68
|
+
#
|
69
|
+
# * <tt>:connected</tt> matches the `[connected]` attribute
|
70
|
+
# * <tt>:channel</tt> matches the `[channel]` attribute. Can be a Class,
|
71
|
+
# String, Symbol, or Regexp
|
72
|
+
# * <tt>:signed_stream_name</tt> matches the element's `[signed-stream-name]`
|
73
|
+
# attribute. Can be of any type that is a valid
|
74
|
+
# argument to <tt>Turbo::Streams::StreamName#signed_stream_name</tt>.
|
75
|
+
#
|
76
|
+
# In addition to the filters listed above, accepts any valid Capybara global
|
77
|
+
# filter option.
|
78
|
+
def assert_no_turbo_cable_stream_source(...)
|
79
|
+
assert_no_selector(:turbo_cable_stream_source, ...)
|
80
|
+
end
|
81
|
+
|
82
|
+
Capybara.add_selector :turbo_cable_stream_source do
|
83
|
+
xpath do |locator|
|
84
|
+
xpath = XPath.descendant.where(XPath.local_name == "turbo-cable-stream-source")
|
85
|
+
xpath.where(SignedStreamNameConditions.new(locator).reduce(:|))
|
86
|
+
end
|
87
|
+
|
88
|
+
expression_filter :connected do |xpath, value|
|
89
|
+
builder(xpath).add_attribute_conditions(connected: value)
|
90
|
+
end
|
91
|
+
|
92
|
+
expression_filter :channel do |xpath, value|
|
93
|
+
builder(xpath).add_attribute_conditions(channel: value.try(:name) || value)
|
94
|
+
end
|
95
|
+
|
96
|
+
expression_filter :signed_stream_name do |xpath, value|
|
97
|
+
case value
|
98
|
+
when TrueClass, FalseClass, NilClass, Regexp
|
99
|
+
builder(xpath).add_attribute_conditions("signed-stream-name": value)
|
100
|
+
else
|
101
|
+
xpath.where(SignedStreamNameConditions.new(value).reduce(:|))
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
class SignedStreamNameConditions # :nodoc:
|
107
|
+
include Turbo::Streams::StreamName, Enumerable
|
108
|
+
|
109
|
+
def initialize(value)
|
110
|
+
@value = value
|
111
|
+
end
|
112
|
+
|
113
|
+
def attribute
|
114
|
+
XPath.attr(:"signed-stream-name")
|
115
|
+
end
|
116
|
+
|
117
|
+
def each
|
118
|
+
if @value.is_a?(String)
|
119
|
+
yield attribute == @value
|
120
|
+
yield attribute == signed_stream_name(@value)
|
121
|
+
elsif @value.is_a?(Array) || @value.respond_to?(:to_key)
|
122
|
+
yield attribute == signed_stream_name(@value)
|
123
|
+
elsif @value.present?
|
124
|
+
yield attribute == @value
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
data/lib/turbo/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: turbo-rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.
|
4
|
+
version: 2.0.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sam Stephenson
|
@@ -10,22 +10,8 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2024-09-
|
13
|
+
date: 2024-09-18 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
|
-
- !ruby/object:Gem::Dependency
|
16
|
-
name: activejob
|
17
|
-
requirement: !ruby/object:Gem::Requirement
|
18
|
-
requirements:
|
19
|
-
- - ">="
|
20
|
-
- !ruby/object:Gem::Version
|
21
|
-
version: 6.0.0
|
22
|
-
type: :runtime
|
23
|
-
prerelease: false
|
24
|
-
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
requirements:
|
26
|
-
- - ">="
|
27
|
-
- !ruby/object:Gem::Version
|
28
|
-
version: 6.0.0
|
29
15
|
- !ruby/object:Gem::Dependency
|
30
16
|
name: actionpack
|
31
17
|
requirement: !ruby/object:Gem::Requirement
|
@@ -100,6 +86,7 @@ files:
|
|
100
86
|
- lib/turbo-rails.rb
|
101
87
|
- lib/turbo/broadcastable/test_helper.rb
|
102
88
|
- lib/turbo/engine.rb
|
89
|
+
- lib/turbo/system_test_helper.rb
|
103
90
|
- lib/turbo/test_assertions.rb
|
104
91
|
- lib/turbo/test_assertions/integration_test_assertions.rb
|
105
92
|
- lib/turbo/version.rb
|