Skip to content

Commit

Permalink
Introduce Turbo::Broadcastable::TestHelper
Browse files Browse the repository at this point in the history
The `Turbo::Broadcastable::TestHelper` 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.

Asserts `<turbo-stream>` elements were broadcast:

```ruby
message = Message.find(1)
message.broadcast_replace_to "messages"

assert_turbo_stream_broadcasts "messages"
```

The assertion returns the broadcasts as an Array of <tt>Nokogiri::XML::Element</tt> instances

```ruby
message = Message.find(1)
message.broadcast_append_to "messages"
message.broadcast_prepend_to "messages"

turbo_streams = assert_turbo_stream_broadcasts "messages"

assert_equal "append", turbo_streams.first["action"]
assert_equal "prepend", turbo_streams.second["action"]
```

You can pass a block to run before the assertion:

```ruby
message = Message.find(1)

turbo_streams = assert_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:

```ruby
message = Message.find(1)

turbo_streams = assert_turbo_stream_broadcasts message do
  message.broadcast_replace
end

assert_equal "replace", turbo_streams.first["action"]
```

For the sake of parity, this commit also introduces the
`assert_no_turbo_stream_broadcasts` helper.
  • Loading branch information
seanpdoyle committed May 14, 2023
1 parent ea00f37 commit 4d3d59c
Show file tree
Hide file tree
Showing 4 changed files with 294 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,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
120 changes: 120 additions & 0 deletions lib/turbo/broadcastable/test_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
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
#
# Asserts `<turbo-stream>` elements were broadcast:
#
# message = Message.find(1)
# message.broadcast_replace_to "messages"
#
# assert_turbo_stream_broadcasts "messages"
#
# The assertion returns the broadcasts 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 = assert_turbo_stream_broadcasts "messages"
#
# assert_equal "append", turbo_streams.first["action"]
# assert_equal "prepend", turbo_streams.second["action"]
#
# You can pass a block to run before the assertion:
#
# message = Message.find(1)
#
# turbo_streams = assert_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)
#
# turbo_streams = assert_turbo_stream_broadcasts message do
# message.broadcast_replace
# end
#
# assert_equal "replace", turbo_streams.first["action"]
#
def assert_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.present?, "Expected at least one broadcast on #{stream_name.inspect}, but there were none"

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

document.at("body").element_children
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
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 @@ -69,6 +69,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
172 changes: 172 additions & 0 deletions test/broadcastable/test_helper_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
require "test_helper"

class Turbo::Broadcastable::TestHelper::AssertTurboStreamBroadcastsTest < ActiveSupport::TestCase
include Turbo::Broadcastable::TestHelper

test "#assert_turbo_stream_broadcasts returns <turbo-stream> elements broadcast on a stream name" do
message = Message.new(id: 1)

message.broadcast_replace_to "messages"
message.broadcast_remove_to "messages"
replace, remove, *rest = assert_turbo_stream_broadcasts "messages"

assert_empty rest
assert_equal "replace", replace["action"]
assert_equal "remove", remove["action"]
assert_not_empty replace.at("template").element_children
assert_nil remove.at("template")
end

test "#assert_turbo_stream_broadcasts fails when no broadcasts happened on a stream name" do
assert_raises MiniTest::Assertion do
assert_turbo_stream_broadcasts "messages"
end
end

test "#assert_turbo_stream_broadcasts returns <turbo-stream> elements broadcast on a stream object" do
message = Message.new(id: 1)

message.broadcast_replace
message.broadcast_remove
replace, remove, *rest = assert_turbo_stream_broadcasts message

assert_empty rest
assert_equal "replace", replace["action"]
assert_equal "remove", remove["action"]
assert_not_empty replace.at("template").element_children
assert_nil remove.at("template")
end

test "#assert_turbo_stream_broadcasts returns <turbo-stream> elements broadcast on an Array of stream objects" do
message = Message.new(id: 1)

message.broadcast_replace_to [message, :special]
message.broadcast_remove_to [message, :special]
replace, remove, *rest = assert_turbo_stream_broadcasts [message, :special]

assert_empty rest
assert_equal "replace", replace["action"]
assert_equal "remove", remove["action"]
assert_not_empty replace.at("template").element_children
assert_nil remove.at("template")
end

test "#assert_turbo_stream_broadcasts returns <turbo-stream> elements broadcast on a stream name from a block" do
message = Message.new(id: 1)

replace, remove, *rest = assert_turbo_stream_broadcasts "messages" do
message.broadcast_replace_to "messages"
message.broadcast_remove_to "messages"
end

assert_equal "replace", replace["action"]
assert_equal "remove", remove["action"]
assert_empty rest
end

test "#assert_turbo_stream_broadcasts returns <turbo-stream> elements broadcast on a stream object from a block" do
message = Message.new(id: 1)

replace, remove, *rest = assert_turbo_stream_broadcasts message do
message.broadcast_replace
message.broadcast_remove
end

assert_empty rest
assert_equal "replace", replace["action"]
assert_equal "remove", remove["action"]
assert_not_empty replace.at("template").element_children
assert_nil remove.at("template")
end

test "#assert_turbo_stream_broadcasts returns <turbo-stream> elements broadcast on an Array of stream objects from a block" do
message = Message.new(id: 1)

replace, remove, *rest = assert_turbo_stream_broadcasts [message, :special] do
message.broadcast_replace_to [message, :special]
message.broadcast_remove_to [message, :special]
end

assert_empty rest
assert_equal "replace", replace["action"]
assert_equal "remove", remove["action"]
assert_not_empty replace.at("template").element_children
assert_nil remove.at("template")
end

test "#assert_turbo_stream_broadcasts fails when no broadcasts happened on a stream name from a block" do
assert_raises MiniTest::Assertion do
assert_turbo_stream_broadcasts "messages" do
# no-op
end
end
end
end

class Turbo::Broadcastable::TestHelper::AssertNoTurboStreamBroadcastsTest < ActiveSupport::TestCase
include Turbo::Broadcastable::TestHelper

test "#assert_no_turbo_stream_broadcasts asserts no broadcasts with a stream name" do
assert_no_turbo_stream_broadcasts "messages"
end

test "#assert_no_turbo_stream_broadcasts asserts no broadcasts with a stream name from a block" do
assert_no_turbo_stream_broadcasts "messages" do
# no-op
end
end

test "#assert_no_turbo_stream_broadcasts asserts no broadcasts with a stream object" do
message = Message.new(id: 1)

assert_no_turbo_stream_broadcasts message
end

test "#assert_no_turbo_stream_broadcasts asserts no broadcasts with a stream object from a block" do
message = Message.new(id: 1)

assert_no_turbo_stream_broadcasts message do
# no-op
end
end

test "#assert_no_turbo_stream_broadcasts fails when when a broadcast happened on a stream name" do
message = Message.new(id: 1)

assert_raises MiniTest::Assertion do
message.broadcast_remove_to "messages"

assert_no_turbo_stream_broadcasts "messages"
end
end

test "#assert_no_turbo_stream_broadcasts fails when when a broadcast happened on a stream name from a block" do
message = Message.new(id: 1)

assert_raises MiniTest::Assertion do
assert_no_turbo_stream_broadcasts "messages" do
message.broadcast_remove_to "messages"
end
end
end

test "#assert_no_turbo_stream_broadcasts fails when when a broadcast happened on a stream object" do
message = Message.new(id: 1)

assert_raises MiniTest::Assertion do
message.broadcast_remove

assert_no_turbo_stream_broadcasts message
end
end

test "#assert_no_turbo_stream_broadcasts fails when when a broadcast happened on a stream object from a block" do
message = Message.new(id: 1)

assert_raises MiniTest::Assertion do
assert_no_turbo_stream_broadcasts message do
message.broadcast_remove
end
end
end
end

0 comments on commit 4d3d59c

Please sign in to comment.