From a9e011de75badb2ae71741846f9fa89bcdb84901 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 20 Sep 2023 14:40:58 +0200 Subject: [PATCH] Support for page refreshes and broadcasting This PR is the Rails companion for the Turbo changes to add page refreshes. ```ruby turbo_refreshes_with scroll method: :morph, scroll: :preserve ``` This adds new Active Record helpers to broadcast page refreshes from models: ```ruby class Board broadcast_refreshes end ``` This works great in hierarchical structures, where child record touch parent records automatically to invalidate cache: ```ruby class Column belongs_to :board, touch: true # +Board+ will trigger a page refresh on column changes end ``` You can also specify the streamable declaratively: ```ruby class Column belongs_to :board broadcast_refreshes_to :board end ``` There are also instance-level companion methods to broadcast page refreshes: - `broadcast_refresh_later` - `broadcast_refresh_later_to(*streamables)` This PR introduces a new mechanism to suppress broadcasting of turbo treams for arbitrary blocks of code: ```ruby Recording.suppressing_turbo_broadcasts do ... end ``` When broadcasting page refreshes, the system will automatically debounce multiple calls in a row to only broadcast the last one. This is meant for scenarios where you process records in mass. Because of the nature of such signals, it makes no sense to broadcast them repeatedly and individually. --- app/channels/turbo/streams/broadcasts.rb | 8 + .../concerns/turbo/request_id_tracking.rb | 12 + app/helpers/turbo/drive_helper.rb | 8 + app/helpers/turbo/streams/action_helper.rb | 6 +- .../turbo/streams/broadcast_stream_job.rb | 7 + app/models/concerns/turbo/broadcastable.rb | 91 +++++-- lib/turbo-rails.rb | 9 + lib/turbo/engine.rb | 6 + test/current_request_id_test.rb | 28 +++ test/drive/drive_helper_test.rb | 20 ++ .../app/controllers/request_ids_controller.rb | 5 + test/dummy/app/views/trays/index.html.erb | 1 + test/dummy/config/routes.rb | 1 + test/refreshes/request_id_tracking_test.rb | 8 + test/streams/action_helper_test.rb | 13 + test/streams/broadcastable_test.rb | 227 +++++++++++++++++- test/streams/streams_channel_test.rb | 14 ++ test/test_helper.rb | 4 + 18 files changed, 449 insertions(+), 19 deletions(-) create mode 100644 app/controllers/concerns/turbo/request_id_tracking.rb create mode 100644 app/jobs/turbo/streams/broadcast_stream_job.rb create mode 100644 test/current_request_id_test.rb create mode 100644 test/dummy/app/controllers/request_ids_controller.rb create mode 100644 test/refreshes/request_id_tracking_test.rb diff --git a/app/channels/turbo/streams/broadcasts.rb b/app/channels/turbo/streams/broadcasts.rb index 278e0fbf..3aecebad 100644 --- a/app/channels/turbo/streams/broadcasts.rb +++ b/app/channels/turbo/streams/broadcasts.rb @@ -33,6 +33,10 @@ def broadcast_prepend_to(*streamables, **opts) broadcast_action_to(*streamables, action: :prepend, **opts) end + def broadcast_refresh_to(*streamables, **opts) + broadcast_stream_to(*streamables, content: turbo_stream_refresh_tag) + end + def broadcast_action_to(*streamables, action:, target: nil, targets: nil, attributes: {}, **rendering) broadcast_stream_to(*streamables, content: turbo_stream_action_tag(action, target: target, targets: targets, template: rendering.delete(:content) || rendering.delete(:html) || (rendering[:render] != false && rendering.any? ? render_format(:html, **rendering) : nil), @@ -64,6 +68,10 @@ def broadcast_prepend_later_to(*streamables, **opts) broadcast_action_later_to(*streamables, action: :prepend, **opts) end + def broadcast_refresh_later_to(*streamables, **opts) + Turbo::Streams::BroadcastStreamJob.perform_later stream_name_from(streamables), content: turbo_stream_refresh_tag(**opts) + end + def broadcast_action_later_to(*streamables, action:, target: nil, targets: nil, attributes: {}, **rendering) Turbo::Streams::ActionBroadcastJob.perform_later \ stream_name_from(streamables), action: action, target: target, targets: targets, attributes: attributes, **rendering diff --git a/app/controllers/concerns/turbo/request_id_tracking.rb b/app/controllers/concerns/turbo/request_id_tracking.rb new file mode 100644 index 00000000..2f41acb4 --- /dev/null +++ b/app/controllers/concerns/turbo/request_id_tracking.rb @@ -0,0 +1,12 @@ +module Turbo::RequestIdTracking + extend ActiveSupport::Concern + + included do + around_action :turbo_tracking_request_id + end + + private + def turbo_tracking_request_id(&block) + Turbo.with_request_id(request.headers["X-Turbo-Request-Id"], &block) + end +end diff --git a/app/helpers/turbo/drive_helper.rb b/app/helpers/turbo/drive_helper.rb index 9aaa0a37..66e5994d 100644 --- a/app/helpers/turbo/drive_helper.rb +++ b/app/helpers/turbo/drive_helper.rb @@ -26,4 +26,12 @@ def turbo_exempts_page_from_preview def turbo_page_requires_reload provide :head, tag.meta(name: "turbo-visit-control", content: "reload") end + + def turbo_refreshes_with(method: :replace, scroll: :reset) + raise ArgumentError, "Invalid refresh option '#{method}'" unless method.in?(%i[ replace morph ]) + raise ArgumentError, "Invalid scroll option '#{scroll}'" unless scroll.in?(%i[ reset preserve ]) + + provide :head, tag.meta(name: "turbo-refresh-method", content: method) + provide :head, tag.meta(name: "turbo-refresh-scroll", content: scroll) + end end diff --git a/app/helpers/turbo/streams/action_helper.rb b/app/helpers/turbo/streams/action_helper.rb index 37e6e545..a43255a9 100644 --- a/app/helpers/turbo/streams/action_helper.rb +++ b/app/helpers/turbo/streams/action_helper.rb @@ -24,7 +24,7 @@ module Turbo::Streams::ActionHelper # # => # def turbo_stream_action_tag(action, target: nil, targets: nil, template: nil, **attributes) - template = action.to_sym == :remove ? "" : tag.template(template.to_s.html_safe) + template = action.to_sym.in?(%i[ remove refresh ]) ? "" : tag.template(template.to_s.html_safe) if target = convert_to_turbo_stream_dom_id(target) tag.turbo_stream(template, **attributes, action: action, target: target) @@ -35,6 +35,10 @@ def turbo_stream_action_tag(action, target: nil, targets: nil, template: nil, ** end end + def turbo_stream_refresh_tag(**attributes) + turbo_stream_action_tag(:refresh, **{ "request-id": Turbo.current_request_id }.compact, **attributes) + end + private def convert_to_turbo_stream_dom_id(target, include_selector: false) if Array(target).any? { |value| value.respond_to?(:to_key) } diff --git a/app/jobs/turbo/streams/broadcast_stream_job.rb b/app/jobs/turbo/streams/broadcast_stream_job.rb new file mode 100644 index 00000000..64cd8378 --- /dev/null +++ b/app/jobs/turbo/streams/broadcast_stream_job.rb @@ -0,0 +1,7 @@ +class Turbo::Streams::BroadcastStreamJob < ActiveJob::Base + discard_on ActiveJob::DeserializationError + + def perform(stream, content:) + Turbo::StreamsChannel.broadcast_stream_to(stream, content: content) + end +end diff --git a/app/models/concerns/turbo/broadcastable.rb b/app/models/concerns/turbo/broadcastable.rb index 2af2eeea..78735d8b 100644 --- a/app/models/concerns/turbo/broadcastable.rb +++ b/app/models/concerns/turbo/broadcastable.rb @@ -75,9 +75,32 @@ # In addition to the four basic actions, you can also use broadcast_render, # broadcast_render_to broadcast_render_later, and broadcast_render_later_to # to render a turbo stream template with multiple actions. +# +# == Suppressing broadcasts +# +# Sometimes, you need to disable broadcasts in certain scenarios. You can use .suppressing_turbo_broadcasts to create +# execution contexts where broadcasts are disabled: +# +# class Message < ApplicationRecord +# after_create_commit :update_message +# +# private +# def update_message +# broadcast_replace_to(user, :message, target: "message", renderable: MessageComponent.new) +# end +# end +# +# Message.suppressing_turbo_broadcasts do +# Message.create!(board: board) # This won't broadcast the replace action +# end module Turbo::Broadcastable extend ActiveSupport::Concern + included do + thread_mattr_accessor :suppressed_turbo_broadcasts, instance_accessor: false + delegate :suppressed_turbo_broadcasts?, to: "self.class" + end + module ClassMethods # Configures the model to broadcast creates, updates, and destroys to a stream name derived at runtime by the # stream symbol invocation. By default, the creates are appended to a dom id target name derived from @@ -112,10 +135,34 @@ def broadcasts(stream = model_name.plural, inserts_by: :append, target: broadcas after_destroy_commit -> { broadcast_remove } end + # Configures the model to broadcast a "page refresh" on creates, updates, and destroys to a stream + # name derived at runtime by the stream symbol invocation. + def broadcasts_refreshes_to(stream) + after_commit -> { broadcast_refresh_later_to(stream.try(:call, self) || send(stream)) } + end + + # Same as #broadcasts_refreshes_to, but the designated stream for page refreshes is automatically set to + # the current model. + def broadcasts_refreshes + after_commit -> { broadcast_refresh_later } + end + # All default targets will use the return of this method. Overwrite if you want something else than model_name.plural. def broadcast_target_default model_name.plural end + + # Executes +block+ preventing both synchronous and asynchronous broadcasts from this model. + def suppressing_turbo_broadcasts(&block) + original, self.suppressed_turbo_broadcasts = self.suppressed_turbo_broadcasts, true + yield + ensure + self.suppressed_turbo_broadcasts = original + end + + def suppressed_turbo_broadcasts? + suppressed_turbo_broadcasts + end end # Remove this broadcastable model from the dom for subscribers of the stream name identified by the passed streamables. @@ -124,7 +171,7 @@ def broadcast_target_default # # Sends to the stream named "identity:2:clearances" # clearance.broadcast_remove_to examiner.identity, :clearances def broadcast_remove_to(*streamables, target: self) - Turbo::StreamsChannel.broadcast_remove_to(*streamables, target: target) + Turbo::StreamsChannel.broadcast_remove_to(*streamables, target: target) unless suppressed_turbo_broadcasts? end # Same as #broadcast_remove_to, but the designated stream is automatically set to the current model. @@ -143,7 +190,7 @@ def broadcast_remove # # to the stream named "identity:2:clearances" # clearance.broadcast_replace_to examiner.identity, :clearances, partial: "clearances/other_partial", locals: { a: 1 } def broadcast_replace_to(*streamables, **rendering) - Turbo::StreamsChannel.broadcast_replace_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) + Turbo::StreamsChannel.broadcast_replace_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts? end # Same as #broadcast_replace_to, but the designated stream is automatically set to the current model. @@ -162,7 +209,7 @@ def broadcast_replace(**rendering) # # to the stream named "identity:2:clearances" # clearance.broadcast_update_to examiner.identity, :clearances, partial: "clearances/other_partial", locals: { a: 1 } def broadcast_update_to(*streamables, **rendering) - Turbo::StreamsChannel.broadcast_update_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) + Turbo::StreamsChannel.broadcast_update_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts? end # Same as #broadcast_update_to, but the designated stream is automatically set to the current model. @@ -215,7 +262,7 @@ def broadcast_after_to(*streamables, target:, **rendering) # clearance.broadcast_append_to examiner.identity, :clearances, target: "clearances", # partial: "clearances/other_partial", locals: { a: 1 } def broadcast_append_to(*streamables, target: broadcast_target_default, **rendering) - Turbo::StreamsChannel.broadcast_append_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) + Turbo::StreamsChannel.broadcast_append_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts? end # Same as #broadcast_append_to, but the designated stream is automatically set to the current model. @@ -236,7 +283,7 @@ def broadcast_append(target: broadcast_target_default, **rendering) # clearance.broadcast_prepend_to examiner.identity, :clearances, target: "clearances", # partial: "clearances/other_partial", locals: { a: 1 } def broadcast_prepend_to(*streamables, target: broadcast_target_default, **rendering) - Turbo::StreamsChannel.broadcast_prepend_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) + Turbo::StreamsChannel.broadcast_prepend_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts? end # Same as #broadcast_prepend_to, but the designated stream is automatically set to the current model. @@ -244,13 +291,21 @@ def broadcast_prepend(target: broadcast_target_default, **rendering) broadcast_prepend_to self, target: target, **rendering end + def broadcast_refresh_to(*streamables) + Turbo::StreamsChannel.broadcast_refresh_to *streamables unless suppressed_turbo_broadcasts? + end + + def broadcast_refresh + broadcast_refresh_to self + end + # Broadcast a named action, allowing for dynamic dispatch, instead of using the concrete action methods. Examples: # # # Sends # # to the stream named "identity:2:clearances" # clearance.broadcast_action_to examiner.identity, :clearances, action: :prepend, target: "clearances" def broadcast_action_to(*streamables, action:, target: broadcast_target_default, attributes: {}, **rendering) - Turbo::StreamsChannel.broadcast_action_to(*streamables, action: action, target: target, attributes: attributes, **broadcast_rendering_with_defaults(rendering)) + Turbo::StreamsChannel.broadcast_action_to(*streamables, action: action, target: target, attributes: attributes, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts? end # Same as #broadcast_action_to, but the designated stream is automatically set to the current model. @@ -261,7 +316,7 @@ def broadcast_action(action, target: broadcast_target_default, attributes: {}, * # Same as broadcast_replace_to but run asynchronously via a Turbo::Streams::BroadcastJob. def broadcast_replace_later_to(*streamables, **rendering) - Turbo::StreamsChannel.broadcast_replace_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) + Turbo::StreamsChannel.broadcast_replace_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts? end # Same as #broadcast_replace_later_to, but the designated stream is automatically set to the current model. @@ -271,7 +326,7 @@ def broadcast_replace_later(**rendering) # Same as broadcast_update_to but run asynchronously via a Turbo::Streams::BroadcastJob. def broadcast_update_later_to(*streamables, **rendering) - Turbo::StreamsChannel.broadcast_update_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) + Turbo::StreamsChannel.broadcast_update_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts? end # Same as #broadcast_update_later_to, but the designated stream is automatically set to the current model. @@ -281,7 +336,7 @@ def broadcast_update_later(**rendering) # Same as broadcast_append_to but run asynchronously via a Turbo::Streams::BroadcastJob. def broadcast_append_later_to(*streamables, target: broadcast_target_default, **rendering) - Turbo::StreamsChannel.broadcast_append_later_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) + Turbo::StreamsChannel.broadcast_append_later_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts? end # Same as #broadcast_append_later_to, but the designated stream is automatically set to the current model. @@ -291,7 +346,7 @@ def broadcast_append_later(target: broadcast_target_default, **rendering) # Same as broadcast_prepend_to but run asynchronously via a Turbo::Streams::BroadcastJob. def broadcast_prepend_later_to(*streamables, target: broadcast_target_default, **rendering) - Turbo::StreamsChannel.broadcast_prepend_later_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) + Turbo::StreamsChannel.broadcast_prepend_later_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts? end # Same as #broadcast_prepend_later_to, but the designated stream is automatically set to the current model. @@ -299,9 +354,17 @@ def broadcast_prepend_later(target: broadcast_target_default, **rendering) broadcast_prepend_later_to self, target: target, **rendering end + def broadcast_refresh_later_to(*streamables, target: broadcast_target_default, **rendering) + Turbo::StreamsChannel.broadcast_refresh_later_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering).merge(request_id: Turbo.current_request_id)) unless suppressed_turbo_broadcasts? + end + + def broadcast_refresh_later(target: broadcast_target_default, **rendering) + broadcast_refresh_later_to self, target: target, **rendering + end + # Same as broadcast_action_to but run asynchronously via a Turbo::Streams::BroadcastJob. def broadcast_action_later_to(*streamables, action:, target: broadcast_target_default, attributes: {}, **rendering) - Turbo::StreamsChannel.broadcast_action_later_to(*streamables, action: action, target: target, attributes: attributes, **broadcast_rendering_with_defaults(rendering)) + Turbo::StreamsChannel.broadcast_action_later_to(*streamables, action: action, target: target, attributes: attributes, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts? end # Same as #broadcast_action_later_to, but the designated stream is automatically set to the current model. @@ -337,7 +400,7 @@ def broadcast_render(**rendering) # desireable for model callbacks, certainly not if those callbacks are inside of a transaction. Most of the time you should # be using `broadcast_render_later_to`, unless you specifically know why synchronous rendering is needed. def broadcast_render_to(*streamables, **rendering) - Turbo::StreamsChannel.broadcast_render_to(*streamables, **broadcast_rendering_with_defaults(rendering)) + Turbo::StreamsChannel.broadcast_render_to(*streamables, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts? end # Same as broadcast_action_to but run asynchronously via a Turbo::Streams::BroadcastJob. @@ -348,7 +411,7 @@ def broadcast_render_later(**rendering) # Same as broadcast_render_later but run with the added option of naming the stream using the passed # streamables. def broadcast_render_later_to(*streamables, **rendering) - Turbo::StreamsChannel.broadcast_render_later_to(*streamables, **broadcast_rendering_with_defaults(rendering)) + Turbo::StreamsChannel.broadcast_render_later_to(*streamables, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts? end @@ -361,7 +424,7 @@ def broadcast_rendering_with_defaults(options) options.tap do |o| # Add the current instance into the locals with the element name (which is the un-namespaced name) # as the key. This parallels how the ActionView::ObjectRenderer would create a local variable. - o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.element.to_sym => self) + o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.element.to_sym => self, request_id: Turbo.current_request_id).compact if o[:html] || o[:partial] return o diff --git a/lib/turbo-rails.rb b/lib/turbo-rails.rb index 1a50bd7c..6c401aae 100644 --- a/lib/turbo-rails.rb +++ b/lib/turbo-rails.rb @@ -5,6 +5,8 @@ module Turbo mattr_accessor :draw_routes, default: true + thread_mattr_accessor :current_request_id + class << self attr_writer :signed_stream_verifier_key @@ -15,5 +17,12 @@ def signed_stream_verifier def signed_stream_verifier_key @signed_stream_verifier_key or raise ArgumentError, "Turbo requires a signed_stream_verifier_key" end + + def with_request_id(request_id) + old_request_id, self.current_request_id = self.current_request_id, request_id + yield + ensure + self.current_request_id = old_request_id + end end end diff --git a/lib/turbo/engine.rb b/lib/turbo/engine.rb index 487bf39b..74c4cd79 100644 --- a/lib/turbo/engine.rb +++ b/lib/turbo/engine.rb @@ -46,6 +46,12 @@ class Engine < Rails::Engine end end + initializer "turbo.request_id_tracking" do + ActiveSupport.on_load(:action_controller) do + include Turbo::RequestIdTracking + end + end + initializer "turbo.broadcastable" do ActiveSupport.on_load(:active_record) do include Turbo::Broadcastable diff --git a/test/current_request_id_test.rb b/test/current_request_id_test.rb new file mode 100644 index 00000000..9adbbad0 --- /dev/null +++ b/test/current_request_id_test.rb @@ -0,0 +1,28 @@ +require "test_helper" +require "action_cable" + +class Turbo::CurrentRequestIdTest < ActiveSupport::TestCase + test "sets the current request id for a block of code" do + assert_nil Turbo.current_request_id + + result = Turbo.with_request_id("123") do + assert_equal "123", Turbo.current_request_id + :the_result + end + + assert_equal :the_result, result + assert_nil Turbo.current_request_id + end + + test "raised errors will raise and clear the current request id" do + assert_nil Turbo.current_request_id + + assert_raise "Some error" do + Turbo.with_request_id("123") do + raise "Some error" + end + end + + assert_nil Turbo.current_request_id + end +end diff --git a/test/drive/drive_helper_test.rb b/test/drive/drive_helper_test.rb index d22cc52d..e071ed31 100644 --- a/test/drive/drive_helper_test.rb +++ b/test/drive/drive_helper_test.rb @@ -10,4 +10,24 @@ class Turbo::DriveHelperTest < ActionDispatch::IntegrationTest get trays_path assert_match(//, @response.body) end + + test "configuring refresh strategy" do + get trays_path + assert_match(//, @response.body) + assert_match(//, @response.body) + end +end + +class Turbo::DriverHelperUnitTest < ActionView::TestCase + include Turbo::DriveHelper + + test "validate turbo refresh values" do + assert_raises ArgumentError do + turbo_refreshes_with(method: :invalid) + end + + assert_raises ArgumentError do + turbo_refreshes_with(scroll: :invalid) + end + end end diff --git a/test/dummy/app/controllers/request_ids_controller.rb b/test/dummy/app/controllers/request_ids_controller.rb new file mode 100644 index 00000000..5983dbde --- /dev/null +++ b/test/dummy/app/controllers/request_ids_controller.rb @@ -0,0 +1,5 @@ +class RequestIdsController < ApplicationController + def show + render json: { turbo_frame_request_id: Turbo.current_request_id } + end +end diff --git a/test/dummy/app/views/trays/index.html.erb b/test/dummy/app/views/trays/index.html.erb index c1241bd7..9658cb7d 100644 --- a/test/dummy/app/views/trays/index.html.erb +++ b/test/dummy/app/views/trays/index.html.erb @@ -1,4 +1,5 @@ <% turbo_exempts_page_from_cache %> <% turbo_page_requires_reload %> +<%= turbo_refreshes_with method: :morph, scroll: :preserve %>

Not in the cache!

diff --git a/test/dummy/config/routes.rb b/test/dummy/config/routes.rb index aae1d048..f84b8760 100644 --- a/test/dummy/config/routes.rb +++ b/test/dummy/config/routes.rb @@ -16,4 +16,5 @@ namespace :admin do resources :companies end + resource :request_id end diff --git a/test/refreshes/request_id_tracking_test.rb b/test/refreshes/request_id_tracking_test.rb new file mode 100644 index 00000000..2bb34cdc --- /dev/null +++ b/test/refreshes/request_id_tracking_test.rb @@ -0,0 +1,8 @@ +require "test_helper" + +class Turbo::RequestIdTrackingTest < ActionDispatch::IntegrationTest + test "set the current turbo request id from the value in the X-Turbo-Request-Id header" do + get request_id_path, headers: { "X-Turbo-Request-Id" => "123" } + assert_equal "123", JSON.parse(response.body)["turbo_frame_request_id"] + end +end diff --git a/test/streams/action_helper_test.rb b/test/streams/action_helper_test.rb index d1818cd0..20a78836 100644 --- a/test/streams/action_helper_test.rb +++ b/test/streams/action_helper_test.rb @@ -85,4 +85,17 @@ class Turbo::ActionHelperTest < ActionCable::Channel::TestCase assert_equal "", action end + + test "turbo stream refresh tag" do + action = turbo_stream_refresh_tag + + assert_equal "", action + end + + test "turbo stream refresh tag that carries the current request id" do + Turbo.current_request_id = "123" + action = turbo_stream_refresh_tag + + assert_equal "", action + end end diff --git a/test/streams/broadcastable_test.rb b/test/streams/broadcastable_test.rb index 083f2b74..e13bbc1a 100644 --- a/test/streams/broadcastable_test.rb +++ b/test/streams/broadcastable_test.rb @@ -96,6 +96,18 @@ class Turbo::BroadcastableTest < ActionCable::Channel::TestCase end end + test "broadcasting refresh to stream now" do + assert_broadcast_on "stream", turbo_stream_refresh_tag do + @message.broadcast_refresh_to "stream" + end + end + + test "broadcasting refresh now" do + assert_broadcast_on @message.to_gid_param, turbo_stream_refresh_tag do + @message.broadcast_refresh + end + end + test "broadcasting action to stream now" do assert_broadcast_on "stream", turbo_stream_action_tag("prepend", target: "messages", template: render(@message)) do @message.broadcast_action_to "stream", action: "prepend" @@ -248,8 +260,8 @@ class Turbo::BroadcastableCommentTest < ActionCable::Channel::TestCase test "updating a comment broadcasts" do comment = @article.comments.create!(body: "random") - stream = "#{@article.to_gid_param}:comments" - target = "comment_#{comment.id}" + stream = "#{@article.to_gid_param}:comments" + target = "comment_#{comment.id}" assert_broadcast_on stream, turbo_stream_action_tag("replace", target: target, template: %(

precise

\n)) do perform_enqueued_jobs do @@ -260,11 +272,218 @@ class Turbo::BroadcastableCommentTest < ActionCable::Channel::TestCase test "destroying a comment broadcasts" do comment = @article.comments.create!(body: "comment") - stream = "#{@article.to_gid_param}:comments" - target = "comment_#{comment.id}" + stream = "#{@article.to_gid_param}:comments" + target = "comment_#{comment.id}" assert_broadcast_on stream, turbo_stream_action_tag("remove", target: target) do comment.destroy! end end end + +class Turbo::SuppressingBroadcastsTest < ActionCable::Channel::TestCase + include ActiveJob::TestHelper, Turbo::Streams::ActionHelper + + setup { @message = Message.new(id: 1, content: "Hello!") } + + test "suppressing broadcasting remove to stream now" do + assert_no_broadcasts_when_suppressing do + @message.broadcast_remove_to "stream" + end + end + + test "suppressing broadcasting remove now" do + assert_no_broadcasts_when_suppressing do + @message.broadcast_remove + end + end + + test "suppressing broadcasting replace to stream now" do + assert_no_broadcasts_when_suppressing do + @message.broadcast_replace_to "stream" + end + end + + test "suppressing broadcasting replace to stream later" do + assert_no_broadcasts_later_when_supressing do + @message.broadcast_replace_later_to "stream" + end + end + + test "suppressing broadcasting replace now" do + assert_no_broadcasts_when_suppressing do + @message.broadcast_replace + end + end + + test "suppressing broadcasting replace later" do + assert_no_broadcasts_later_when_supressing do + @message.broadcast_replace_later + end + end + + test "suppressing broadcasting update to stream now" do + assert_no_broadcasts_when_suppressing do + @message.broadcast_update_to "stream" + end + end + + test "suppressing broadcasting update to stream later" do + assert_no_broadcasts_later_when_supressing do + @message.broadcast_update_later_to "stream" + end + end + + test "suppressing broadcasting update now" do + assert_no_broadcasts_when_suppressing do + @message.broadcast_update + end + end + + test "suppressing broadcasting update later" do + assert_no_broadcasts_later_when_supressing do + @message.broadcast_update_later + end + end + + test "suppressing broadcasting before to stream now" do + assert_no_broadcasts_when_suppressing do + @message.broadcast_before_to "stream", target: "message_1" + end + end + + test "suppressing broadcasting after to stream now" do + assert_no_broadcasts_when_suppressing do + @message.broadcast_after_to "stream", target: "message_1" + end + end + + test "suppressing broadcasting append to stream now" do + assert_no_broadcasts_when_suppressing do + @message.broadcast_append_to "stream" + end + end + + test "suppressing broadcasting append to stream later" do + assert_no_broadcasts_later_when_supressing do + @message.broadcast_append_later_to "stream" + end + end + + test "suppressing broadcasting append now" do + assert_no_broadcasts_when_suppressing do + @message.broadcast_append + end + end + + test "suppressing broadcasting append later" do + assert_no_broadcasts_later_when_supressing do + @message.broadcast_append_later + end + end + + test "suppressing broadcasting prepend to stream now" do + assert_no_broadcasts_when_suppressing do + @message.broadcast_prepend_to "stream" + end + end + + test "suppressing broadcasting prepend to stream later" do + assert_no_broadcasts_later_when_supressing do + @message.broadcast_prepend_later_to "stream" + end + end + + test "suppressing broadcasting refresh to stream now" do + assert_no_broadcasts_when_suppressing do + @message.broadcast_refresh_to "stream" + end + end + + test "suppressing broadcasting refresh to stream later" do + assert_no_broadcasts_later_when_supressing do + @message.broadcast_refresh_later_to "stream" + end + end + + test "suppressing broadcasting prepend now" do + assert_no_broadcasts_when_suppressing do + @message.broadcast_prepend + end + end + + test "suppressing broadcasting prepend later" do + assert_no_broadcasts_later_when_supressing do + @message.broadcast_prepend_later + end + end + + test "suppressing broadcasting action to stream now" do + assert_no_broadcasts_when_suppressing do + @message.broadcast_action_to "stream", action: "prepend" + end + end + + test "suppressing broadcasting action to stream later" do + assert_no_broadcasts_later_when_supressing do + @message.broadcast_action_later_to "stream", action: "prepend" + end + end + + test "suppressing broadcasting action now" do + assert_no_broadcasts_when_suppressing do + @message.broadcast_action "prepend" + end + end + + test "suppressing broadcasting action later" do + assert_no_broadcasts_later_when_supressing do + @message.broadcast_action_later action: "prepend" + end + end + + test "suppressing broadcast render now" do + assert_no_broadcasts_when_suppressing do + @message.broadcast_render + end + end + + test "suppressing broadcast render later" do + assert_no_broadcasts_later_when_supressing do + @message.broadcast_render_later + end + end + + test "suppressing broadcast render to stream now" do + @profile = Users::Profile.new(id: 1, name: "Ryan") + assert_no_broadcasts_when_suppressing do + @message.broadcast_render_to @profile + end + end + + test "suppressing broadcast render to stream later" do + @profile = Users::Profile.new(id: 1, name: "Ryan") + assert_no_broadcasts_later_when_supressing do + @message.broadcast_render_to @profile + end + end + + private + def assert_no_broadcasts_when_suppressing + assert_no_broadcasts @message.to_gid_param do + Message.suppressing_turbo_broadcasts do + yield + end + end + end + + def assert_no_broadcasts_later_when_supressing + assert_no_broadcasts_when_suppressing do + assert_no_enqueued_jobs do + yield + end + end + end +end + + diff --git a/test/streams/streams_channel_test.rb b/test/streams/streams_channel_test.rb index dea35c6f..9ea562f6 100644 --- a/test/streams/streams_channel_test.rb +++ b/test/streams/streams_channel_test.rb @@ -169,7 +169,21 @@ class Turbo::StreamsChannelTest < ActionCable::Channel::TestCase "stream", targets: ".message", **options end end + end + + test "broadcasting refresh later" do + assert_broadcast_on "stream", turbo_stream_refresh_tag do + perform_enqueued_jobs do + Turbo::StreamsChannel.broadcast_refresh_later_to "stream" + end + end + Turbo.current_request_id = "123" + assert_broadcast_on "stream", turbo_stream_refresh_tag do + perform_enqueued_jobs do + Turbo::StreamsChannel.broadcast_refresh_later_to "stream" + end + end end test "broadcasting action later" do diff --git a/test/test_helper.rb b/test/test_helper.rb index bc2878b8..bbe0f583 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -15,6 +15,10 @@ def render(...) class ActiveSupport::TestCase include ActiveJob::TestHelper + + setup do + Turbo.current_request_id = nil + end end class ActionDispatch::IntegrationTest