Skip to content

Commit

Permalink
Introduce Turbo::Broadcastable::TestHelper (#466)
Browse files Browse the repository at this point in the history
  • Loading branch information
seanpdoyle authored Jul 25, 2023
1 parent f83ca5e commit 00b4f86
Show file tree
Hide file tree
Showing 4 changed files with 438 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ The [`Turbo::TestAssertions`](./lib/turbo/test_assertions.rb) concern provides T

The [`Turbo::TestAssertions::IntegrationTestAssertions`](./lib/turbo/test_assertions/integration_test_assertions.rb) are built on top of `Turbo::TestAssertions`, and add support for passing a `status:` keyword. They are automatically included in [`ActionDispatch::IntegrationTest`](https://edgeguides.rubyonrails.org/testing.html#integration-testing).

The [`Turbo::Broadcastable::TestHelper`](./lib/turbo/broadcastable/test_helper.rb) concern provides Action Cable-aware test helpers that assert that `<turbo-stream>` elements were or were not broadcast over Action Cable. They are not automatically included. To use them in your tests, make sure to `include Turbo::Broadcastable::TestHelper`.

## Development

Expand Down
172 changes: 172 additions & 0 deletions lib/turbo/broadcastable/test_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
module Turbo
module Broadcastable
module TestHelper
extend ActiveSupport::Concern

included do
include ActionCable::TestHelper

include Turbo::Streams::StreamName
end

# Asserts that `<turbo-stream>` elements were broadcast over Action Cable
#
# === Arguments
#
# * <tt>stream_name_or_object</tt> the objects used to generate the
# channel Action Cable name, or the name itself
# * <tt>&block</tt> optional block executed before the
# assertion
#
# === Options
#
# * <tt>count:</tt> the number of `<turbo-stream>` elements that are
# expected to be broadcast
#
# Asserts `<turbo-stream>` elements were broadcast:
#
# message = Message.find(1)
# message.broadcast_replace_to "messages"
#
# assert_turbo_stream_broadcasts "messages"
#
# Asserts that two `<turbo-stream>` elements were broadcast:
#
# message = Message.find(1)
# message.broadcast_replace_to "messages"
# message.broadcast_remove_to "messages"
#
# assert_turbo_stream_broadcasts "messages", count: 2
#
# You can pass a block to run before the assertion:
#
# message = Message.find(1)
#
# assert_turbo_stream_broadcasts "messages" do
# message.broadcast_append_to "messages"
# end
#
# In addition to a String, the helper also accepts an Object or Array to
# determine the name of the channel the elements are broadcast to:
#
# message = Message.find(1)
#
# assert_turbo_stream_broadcasts message do
# message.broadcast_replace
# end
#
def assert_turbo_stream_broadcasts(stream_name_or_object, count: nil, &block)
payloads = capture_turbo_stream_broadcasts(stream_name_or_object, &block)
stream_name = stream_name_from(stream_name_or_object)

if count.nil?
assert_not_empty payloads, "Expected at least one broadcast on #{stream_name.inspect}, but there were none"
else
broadcasts = "Turbo Stream broadcast".pluralize(count)

assert count == payloads.count, "Expected #{count} #{broadcasts} on #{stream_name.inspect}, but there were none"
end
end

# Asserts that no `<turbo-stream>` elements were broadcast over Action Cable
#
# === Arguments
#
# * <tt>stream_name_or_object</tt> the objects used to generate the
# channel Action Cable name, or the name itself
# * <tt>&block</tt> optional block executed before the
# assertion
#
# Asserts that no `<turbo-stream>` elements were broadcast:
#
# message = Message.find(1)
# message.broadcast_replace_to "messages"
#
# assert_no_turbo_stream_broadcasts "messages" # fails with MiniTest::Assertion error
#
# You can pass a block to run before the assertion:
#
# message = Message.find(1)
#
# assert_no_turbo_stream_broadcasts "messages" do
# # do something other than broadcast to "messages"
# end
#
# In addition to a String, the helper also accepts an Object or Array to
# determine the name of the channel the elements are broadcast to:
#
# message = Message.find(1)
#
# assert_no_turbo_stream_broadcasts message do
# # do something other than broadcast to "message_1"
# end
#
def assert_no_turbo_stream_broadcasts(stream_name_or_object, &block)
block&.call

stream_name = stream_name_from(stream_name_or_object)

payloads = broadcasts(stream_name)

assert payloads.empty?, "Expected no broadcasts on #{stream_name.inspect}, but there were #{payloads.count}"
end

# Captures any `<turbo-stream>` elements that were broadcast over Action Cable
#
# === Arguments
#
# * <tt>stream_name_or_object</tt> the objects used to generate the
# channel Action Cable name, or the name itself
# * <tt>&block</tt> optional block to capture broadcasts during execution
#
# Returns any `<turbo-stream>` elements that have been broadcast as an
# Array of <tt>Nokogiri::XML::Element</tt> instances
#
# message = Message.find(1)
# message.broadcast_append_to "messages"
# message.broadcast_prepend_to "messages"
#
# turbo_streams = capture_turbo_stream_broadcasts "messages"
#
# assert_equal "append", turbo_streams.first["action"]
# assert_equal "prepend", turbo_streams.second["action"]
#
# You can pass a block to limit the scope of the broadcasts being captured:
#
# message = Message.find(1)
#
# turbo_streams = capture_turbo_stream_broadcasts "messages" do
# message.broadcast_append_to "messages"
# end
#
# assert_equal "append", turbo_streams.first["action"]
#
# In addition to a String, the helper also accepts an Object or Array to
# determine the name of the channel the elements are broadcast to:
#
# message = Message.find(1)
#
# replace, remove = capture_turbo_stream_broadcasts message do
# message.broadcast_replace
# message.broadcast_remove
# end
#
# assert_equal "replace", replace["action"]
# assert_equal "replace", remove["action"]
#
def capture_turbo_stream_broadcasts(stream_name_or_object, &block)
block&.call

stream_name = stream_name_from(stream_name_or_object)
payloads = broadcasts(stream_name)

payloads.flat_map do |payload|
html = ActiveSupport::JSON.decode(payload)
document = Nokogiri::HTML5.parse(html)

document.at("body").element_children
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/turbo/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class Engine < Rails::Engine
initializer "turbo.test_assertions" do
ActiveSupport.on_load(:active_support_test_case) do
require "turbo/test_assertions"
require "turbo/broadcastable/test_helper"

include Turbo::TestAssertions
end
Expand Down
Loading

0 comments on commit 00b4f86

Please sign in to comment.