turbo-rails 2.0.19 → 2.0.21

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.
@@ -33,8 +33,8 @@ module Turbo::Streams::Broadcasts
33
33
  broadcast_action_to(*streamables, action: :prepend, **opts)
34
34
  end
35
35
 
36
- def broadcast_refresh_to(*streamables, **opts)
37
- broadcast_stream_to(*streamables, content: turbo_stream_refresh_tag)
36
+ def broadcast_refresh_to(*streamables, **attributes)
37
+ broadcast_stream_to(*streamables, content: turbo_stream_refresh_tag(**attributes))
38
38
  end
39
39
 
40
40
  def broadcast_action_to(*streamables, action:, target: nil, targets: nil, attributes: {}, **rendering)
@@ -74,13 +74,13 @@ module Turbo::DriveHelper
74
74
 
75
75
  # Configure method to perform page refreshes. See +turbo_refreshes_with+.
76
76
  def turbo_refresh_method_tag(method = :replace)
77
- raise ArgumentError, "Invalid refresh option '#{method}'" unless method.in?(%i[ replace morph ])
77
+ raise ArgumentError, "Invalid refresh option '#{method}'" unless method.to_sym.in?(%i[ replace morph ])
78
78
  tag.meta(name: "turbo-refresh-method", content: method)
79
79
  end
80
80
 
81
81
  # Configure scroll strategy for page refreshes. See +turbo_refreshes_with+.
82
82
  def turbo_refresh_scroll_tag(scroll = :reset)
83
- raise ArgumentError, "Invalid scroll option '#{scroll}'" unless scroll.in?(%i[ reset preserve ])
83
+ raise ArgumentError, "Invalid scroll option '#{scroll}'" unless scroll.to_sym.in?(%i[ reset preserve ])
84
84
  tag.meta(name: "turbo-refresh-scroll", content: scroll)
85
85
  end
86
86
  end
@@ -33,10 +33,13 @@ module Turbo::FramesHelper
33
33
  # <%= turbo_frame_tag(Article.find(1)) %>
34
34
  # # => <turbo-frame id="article_1"></turbo-frame>
35
35
  #
36
+ # <%= turbo_frame_tag(Article) %>
37
+ # # => <turbo-frame id="new_article"></turbo-frame>
38
+ #
36
39
  # <%= turbo_frame_tag(Article.find(1), "comments") %>
37
40
  # # => <turbo-frame id="comments_article_1"></turbo-frame>
38
41
  def turbo_frame_tag(*ids, src: nil, target: nil, **attributes, &block)
39
- id = ids.first.respond_to?(:to_key) ? ActionView::RecordIdentifier.dom_id(*ids) : ids.join('_')
42
+ id = ids.first.respond_to?(:to_key) || ids.first.is_a?(Class) ? ActionView::RecordIdentifier.dom_id(*ids) : ids.join('_')
40
43
  src = url_for(src) if src.present?
41
44
 
42
45
  tag.turbo_frame(**attributes.merge(id: id, src: src, target: target).compact, &block)
@@ -19,6 +19,9 @@ module Turbo::Streams::ActionHelper
19
19
  # turbo_stream_action_tag "remove", target: message
20
20
  # # => <turbo-stream action="remove" target="message_1"></turbo-stream>
21
21
  #
22
+ # turbo_stream_action_tag "remove", target: Message
23
+ # # => <turbo-stream action="remove" target="new_message"></turbo-stream>
24
+ #
22
25
  # message = Message.find(1)
23
26
  # turbo_stream_action_tag "remove", target: [message, :special]
24
27
  # # => <turbo-stream action="remove" target="special_message_1"></turbo-stream>
@@ -45,7 +48,7 @@ module Turbo::Streams::ActionHelper
45
48
  private
46
49
  def convert_to_turbo_stream_dom_id(target, include_selector: false)
47
50
  target_array = Array.wrap(target)
48
- if target_array.any? { |value| value.respond_to?(:to_key) }
51
+ if target_array.any? { |value| value.respond_to?(:to_key) || value.is_a?(Class) }
49
52
  "#{"#" if include_selector}#{ActionView::RecordIdentifier.dom_id(*target_array)}"
50
53
  else
51
54
  target
@@ -311,7 +311,7 @@ module Turbo::Broadcastable
311
311
  def broadcast_before_to(*streamables, target: nil, targets: nil, **rendering)
312
312
  raise ArgumentError, "at least one of target or targets is required" unless target || targets
313
313
 
314
- Turbo::StreamsChannel.broadcast_before_to(*streamables, **extract_options_and_add_target(rendering.merge(target: target, targets: targets)))
314
+ Turbo::StreamsChannel.broadcast_before_to(*streamables, **extract_options_and_add_target(rendering.merge(target: target, targets: targets))) unless suppressed_turbo_broadcasts?
315
315
  end
316
316
 
317
317
  # Insert a rendering of this broadcastable model after the target identified by it's dom id passed as <tt>target</tt>
@@ -329,7 +329,7 @@ module Turbo::Broadcastable
329
329
  def broadcast_after_to(*streamables, target: nil, targets: nil, **rendering)
330
330
  raise ArgumentError, "at least one of target or targets is required" unless target || targets
331
331
 
332
- Turbo::StreamsChannel.broadcast_after_to(*streamables, **extract_options_and_add_target(rendering.merge(target: target, targets: targets)))
332
+ Turbo::StreamsChannel.broadcast_after_to(*streamables, **extract_options_and_add_target(rendering.merge(target: target, targets: targets))) unless suppressed_turbo_broadcasts?
333
333
  end
334
334
 
335
335
  # Append a rendering of this broadcastable model to the target identified by it's dom id passed as <tt>target</tt>
@@ -378,8 +378,8 @@ module Turbo::Broadcastable
378
378
  #
379
379
  # # Sends <turbo-stream action="refresh"></turbo-stream> to the stream named "identity:2:clearances"
380
380
  # clearance.broadcast_refresh_to examiner.identity, :clearances
381
- def broadcast_refresh_to(*streamables)
382
- Turbo::StreamsChannel.broadcast_refresh_to(*streamables) unless suppressed_turbo_broadcasts?
381
+ def broadcast_refresh_to(*streamables, **attributes)
382
+ Turbo::StreamsChannel.broadcast_refresh_to(*streamables, **attributes) unless suppressed_turbo_broadcasts?
383
383
  end
384
384
 
385
385
  # Same as <tt>#broadcast_refresh_to</tt>, but the designated stream is automatically set to the current model.
@@ -442,8 +442,8 @@ module Turbo::Broadcastable
442
442
  end
443
443
 
444
444
  # Same as <tt>broadcast_refresh_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
445
- def broadcast_refresh_later_to(*streamables)
446
- Turbo::StreamsChannel.broadcast_refresh_later_to(*streamables, request_id: Turbo.current_request_id) unless suppressed_turbo_broadcasts?
445
+ def broadcast_refresh_later_to(*streamables, **attributes)
446
+ Turbo::StreamsChannel.broadcast_refresh_later_to(*streamables, request_id: Turbo.current_request_id, **attributes) unless suppressed_turbo_broadcasts?
447
447
  end
448
448
 
449
449
  # Same as <tt>#broadcast_refresh_later_to</tt>, but the designated stream is automatically set to the current model.
@@ -0,0 +1,17 @@
1
+ # A debouncer that executes immediately without delays or background threads.
2
+ # This doesn't debounce at all, but is safe to use in tests.
3
+ class Turbo::ImmediateDebouncer # :nodoc:
4
+ def initialize(delay: Turbo::Debouncer::DEFAULT_DELAY)
5
+ end
6
+
7
+ def debounce(&block)
8
+ block.call
9
+ end
10
+
11
+ def wait
12
+ end
13
+
14
+ def complete?
15
+ true
16
+ end
17
+ end
@@ -3,6 +3,8 @@
3
3
  class Turbo::ThreadDebouncer
4
4
  delegate :wait, to: :debouncer
5
5
 
6
+ class_attribute :debouncer_class, default: Turbo::Debouncer
7
+
6
8
  def self.for(key, delay: Turbo::Debouncer::DEFAULT_DELAY)
7
9
  Thread.current[key] ||= new(key, Thread.current, delay: delay)
8
10
  end
@@ -11,7 +13,7 @@ class Turbo::ThreadDebouncer
11
13
 
12
14
  def initialize(key, thread, delay: )
13
15
  @key = key
14
- @debouncer = Turbo::Debouncer.new(delay: delay)
16
+ @debouncer = debouncer_class.new(delay: delay)
15
17
  @thread = thread
16
18
  end
17
19
 
@@ -155,10 +155,12 @@ module Turbo
155
155
  # assert_equal "replace", remove["action"]
156
156
  #
157
157
  def capture_turbo_stream_broadcasts(stream_name_or_object, &block)
158
- block&.call
159
-
160
158
  stream_name = stream_name_from(stream_name_or_object)
161
- payloads = broadcasts(stream_name)
159
+ payloads = if block_given?
160
+ new_broadcasts_from(broadcasts(stream_name), stream_name, "capture_turbo_stream_broadcasts", &block)
161
+ else
162
+ broadcasts(stream_name)
163
+ end
162
164
 
163
165
  payloads.flat_map do |payload|
164
166
  html = ActiveSupport::JSON.decode(payload)
data/lib/turbo/engine.rb CHANGED
@@ -18,43 +18,17 @@ module Turbo
18
18
 
19
19
  # If the parent application does not use Active Job, app/jobs cannot
20
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
21
  initializer "turbo.no_active_job", before: :set_eager_load_paths do
30
22
  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
23
+ Rails.autoloaders.once.do_not_eager_load("#{root}/app/jobs")
37
24
  end
38
25
  end
39
26
 
40
27
  # If the parent application does not use Action Cable, app/channels cannot
41
28
  # be eager loaded, because it references the ActionCable constant.
42
- #
43
- # When turbo-rails depends on Rails 7 or above, the entire block can be
44
- # reduced to
45
- #
46
- # unless defined?(ActionCable)
47
- # Rails.autoloaders.once.do_not_eager_load("#{root}/app/channels")
48
- # end
49
- #
50
29
  initializer "turbo.no_action_cable", before: :set_eager_load_paths do
51
30
  unless defined?(ActionCable)
52
- if Rails.autoloaders.zeitwerk_enabled?
53
- Rails.autoloaders.once.do_not_eager_load("#{root}/app/channels")
54
- else
55
- # This else branch only runs in Rails 6.x + classic mode.
56
- config.eager_load_paths.delete("#{root}/app/channels")
57
- end
31
+ Rails.autoloaders.once.do_not_eager_load("#{root}/app/channels")
58
32
  end
59
33
  end
60
34
 
@@ -121,6 +95,9 @@ module Turbo
121
95
  ActiveSupport.on_load(:active_support_test_case) do
122
96
  require "turbo/test_assertions"
123
97
  include Turbo::TestAssertions
98
+
99
+ # Use ImmediateDebouncer in tests to prevent flaky tests from background threads
100
+ Turbo::ThreadDebouncer.debouncer_class = Turbo::ImmediateDebouncer
124
101
  end
125
102
 
126
103
  ActiveSupport.on_load(:action_cable) do
@@ -80,6 +80,8 @@ module Turbo::SystemTestHelper
80
80
  end
81
81
 
82
82
  Capybara.add_selector :turbo_cable_stream_source do
83
+ visible :all
84
+
83
85
  xpath do |locator|
84
86
  xpath = XPath.descendant.where(XPath.local_name == "turbo-cable-stream-source")
85
87
  xpath.where(SignedStreamNameConditions.new(locator).reduce(:|))
@@ -79,5 +79,79 @@ module Turbo
79
79
  selector << %([targets="#{targets}"]) if targets
80
80
  assert_select selector, count: 0
81
81
  end
82
+
83
+ # Assert that the rendered fragment of HTML contains a `<turbo-frame>`
84
+ # element.
85
+ #
86
+ # ==== Arguments
87
+ #
88
+ # * <tt>ids</tt> [String, Array<String, ActiveRecord::Base>] matches the element's <tt>[id]</tt> attribute
89
+ #
90
+ # ==== Options
91
+ #
92
+ # * <tt>:loading</tt> [String] matches the element's <tt>[loading]</tt>
93
+ # attribute
94
+ # * <tt>:src</tt> [String] matches the element's <tt>[src]</tt> attribute
95
+ # * <tt>:target</tt> [String] matches the element's <tt>[target]</tt>
96
+ # attribute
97
+ # * <tt>:count</tt> [Integer] indicates how many turbo frames are expected.
98
+ # Defaults to <tt>1</tt>.
99
+ #
100
+ # Given the following HTML fragment:
101
+ #
102
+ # <turbo-frame id="example" target="_top"></turbo-frame>
103
+ #
104
+ # The following assertion would pass:
105
+ #
106
+ # assert_turbo_frame id: "example", target: "_top"
107
+ #
108
+ # You can also pass a block make assertions about the contents of the
109
+ # element. Given the following HTML fragment:
110
+ #
111
+ # <turbo-frame id="example">
112
+ # <p>Hello!</p>
113
+ # </turbo-frame>
114
+ #
115
+ # The following assertion would pass:
116
+ #
117
+ # assert_turbo_frame id: "example" do
118
+ # assert_select "p", text: "Hello!"
119
+ # end
120
+ #
121
+ def assert_turbo_frame(*ids, loading: nil, src: nil, target: nil, count: 1, &block)
122
+ id = ids.first.respond_to?(:to_key) ? ActionView::RecordIdentifier.dom_id(*ids) : ids.join('_')
123
+ selector = %(turbo-frame[id="#{id}"])
124
+ selector << %([loading="#{loading}"]) if loading
125
+ selector << %([src="#{src}"]) if src
126
+ selector << %([target="#{target}"]) if target
127
+ assert_select selector, count: count, &block
128
+ end
129
+
130
+ # Assert that the rendered fragment of HTML does not contain a `<turbo-frame>`
131
+ # element.
132
+ #
133
+ # ==== Arguments
134
+ #
135
+ # * <tt>ids</tt> [String, Array<String, ActiveRecord::Base>] matches the <tt>[id]</tt> attribute
136
+ #
137
+ # ==== Options
138
+ #
139
+ # * <tt>:loading</tt> [String] matches the element's <tt>[loading]</tt>
140
+ # attribute
141
+ # * <tt>:src</tt> [String] matches the element's <tt>[src]</tt> attribute
142
+ # * <tt>:target</tt> [String] matches the element's <tt>[target]</tt>
143
+ # attribute
144
+ #
145
+ # Given the following HTML fragment:
146
+ #
147
+ # <turbo-frame id="example" target="_top"></turbo-frame>
148
+ #
149
+ # The following assertion would fail:
150
+ #
151
+ # assert_no_turbo_frame id: "example", target: "_top"
152
+ #
153
+ def assert_no_turbo_frame(*ids, **options, &block)
154
+ assert_turbo_frame(*ids, **options, count: 0, &block)
155
+ end
82
156
  end
83
157
  end
data/lib/turbo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Turbo
2
- VERSION = "2.0.19"
2
+ VERSION = "2.0.21"
3
3
  end
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.19
4
+ version: 2.0.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Stephenson
@@ -73,6 +73,7 @@ files:
73
73
  - app/jobs/turbo/streams/broadcast_stream_job.rb
74
74
  - app/models/concerns/turbo/broadcastable.rb
75
75
  - app/models/turbo/debouncer.rb
76
+ - app/models/turbo/immediate_debouncer.rb
76
77
  - app/models/turbo/streams/tag_builder.rb
77
78
  - app/models/turbo/thread_debouncer.rb
78
79
  - app/views/layouts/turbo_rails/frame.html.erb