Skip to content

Commit

Permalink
Introduce Turbo::SystemTestHelper
Browse files Browse the repository at this point in the history
Introduce the `Turbo::SystemTestHelper` module to be included into
[ActionDispatch::SystemTestCase][] when it's available.

The module is named to mimic [ActionText::SystemTestHelper][].

The module defines a `#wait_for_turbo_cable_stream_sources` helper
method extracted from this project's System Test suite. It aims to
synchronize the test harness with Turbo's Action Cable-powered broadcast
support. The method will find all `<turbo-cable-stream-source>` elements
that are present but not yet `[connected]` (returning the results
immediately with Capybara's `:wait`), then wait for them to connect
(using whatever Capybara's configured wait value).

[ActionDispatch::SystemTestCase]: https://edgeapi.rubyonrails.org/classes/ActionDispatch/SystemTestCase.html
[ActionText::SystemTestHelper]: https://edgeapi.rubyonrails.org/classes/ActionText/SystemTestHelper.html
  • Loading branch information
seanpdoyle committed Feb 13, 2024
1 parent 07d3488 commit f9c07f7
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 23 deletions.
67 changes: 64 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@ When the user clicks on the `Edit this todo` link, as a direct response to this

### A note on custom layouts

In order to render turbo frame requests without the application layout, Turbo registers a custom [layout method](https://api.rubyonrails.org/classes/ActionView/Layouts/ClassMethods.html#method-i-layout).
In order to render turbo frame requests without the application layout, Turbo registers a custom [layout method](https://api.rubyonrails.org/classes/ActionView/Layouts/ClassMethods.html#method-i-layout).
If your application uses custom layout resolution, you have to make sure to return `"turbo_rails/frame"` (or `false` for TurboRails < 1.4.0) for turbo frame requests:

```ruby
layout :custom_layout

def custom_layout
return "turbo_rails/frame" if turbo_frame_request?

# ... your custom layout logic
```

Expand All @@ -81,7 +81,7 @@ layout :custom_layout

def custom_layout
return "turbo_rails/frame" if turbo_frame_request?

"some_static_layout"
```

Expand All @@ -100,6 +100,66 @@ This gem provides a `turbo_stream_from` helper to create a turbo stream.
<%# Rest of show here %>
```

### Testing Turbo Stream Broadcasts

Receiving server-generated Turbo Broadcasts requires a connected Web Socket.
Views that render `<turbo-cable-stream-source>` elements with the
`#turbo_stream_from` view helper incur a slight delay before they're ready to
receive broadcasts. In System Tests, that delay can disrupt Capybara's built-in
synchronization mechanisms that wait for or assert on content that's broadcast
over Web Sockets. For example, consider a test that navigates to a page and then
immediately asserts that broadcast content is present:

```ruby
test "renders broadcasted Messages" do
message = Message.new content: "Hello, from Action Cable"

visit "/"
click_link "All Messages"
# … execute server-side code to broadcast a Message
message.save!

assert_text message.content
end
```

If the call to `Message#save!` executes quickly enough, it might beat-out any
`<turbo-cable-stream-source>` elements rendered by the call to `click_link "All
Messages"`.

To wait for any disconnected `<turbo-cable-stream-source>` elements to connect,
call [`#wait_for_turbo_cable_stream_sources`](turbo-rails/blob/wait-for-cable-stream-sourceshttps://github.com/hotwired/turbo-rails/blob/main/lib/turbo/system_test_helper.rb):

```diff
test "renders broadcasted Messages" do
message = Message.new content: "Hello, from Action Cable"

visit "/"
click_link "All Messages"
+ wait_for_turbo_cable_stream_sources
# … execute server-side code to broadcast a Message
message.save!

assert_text message.content
end
```

By default, calls to [`#visit`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara/Session:visit) will wait for all `<turbo-cable-stream-source>` elements to connect. You can control this by modifying the `config.turbo.waiting_capybara_actions`. For example, to wait after calls to [`#click_link`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara/Node/Actions:click_link), add the following to `config/environments/test.rb`:

```ruby
# config/environments/test.rb

config.turbo.waiting_capybara_actions << :click_link
```

To disable automatic waiting, set the configuration to `[]`:

```ruby
# config/environments/test.rb

config.turbo.waiting_capybara_actions = []
```

[See documentation](https://turbo.hotwired.dev/handbook/streams).

## Installation
Expand Down Expand Up @@ -139,6 +199,7 @@ Note that this documentation is updated automatically from the main branch, so i
- [Turbo Test Assertions](https://rubydoc.info/github/hotwired/turbo-rails/main/Turbo/TestAssertions)
- [Turbo Integration Test Assertions](https://rubydoc.info/github/hotwired/turbo-rails/main/Turbo/TestAssertions/IntegrationTestAssertions)
- [Turbo Broadcastable Test Helper](https://rubydoc.info/github/hotwired/turbo-rails/main/Turbo/Broadcastable/TestHelper)
- [Turbo System Test Helper](https://rubydoc.info/github/hotwired/turbo-rails/main/Turbo/SystemTestHelper)

## Compatibility with Rails UJS

Expand Down
20 changes: 20 additions & 0 deletions lib/turbo/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class Engine < Rails::Engine
isolate_namespace Turbo
config.eager_load_namespaces << Turbo
config.turbo = ActiveSupport::OrderedOptions.new
config.turbo.waiting_capybara_actions = %i[visit]
config.autoload_once_paths = %W(
#{root}/app/channels
#{root}/app/controllers
Expand Down Expand Up @@ -111,5 +112,24 @@ class TurboStreamEncoder < IdentityEncoder
end
end
end

initializer "turbo.system_test_helper" do
ActiveSupport.on_load(:action_dispatch_system_test_case) do
require "turbo/system_test_helper"
include Turbo::SystemTestHelper
end
end

config.after_initialize do |app|
ActiveSupport.on_load(:action_dispatch_system_test_case) do
app.config.turbo.waiting_capybara_actions.map do |method|
class_eval <<~RUBY, __FILE__, __LINE__ + 1
def #{method}(...) # def visit(...)
super.tap { wait_for_turbo_cable_stream_sources } # super.tap { wait_for_turbo_cable_stream_sources }
end # end
RUBY
end
end
end
end
end
36 changes: 36 additions & 0 deletions lib/turbo/system_test_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module Turbo::SystemTestHelper
extend ActiveSupport::Concern

# Delay until every `<turbo-cable-stream-source>` element present in the page
# is ready to receive broadcasts
#
# test "renders broadcasted Messages" do
# message = Message.new content: "Hello, from Action Cable"
#
# visit "/"
# click_link "All Messages"
# # … execute server-side code to broadcast a Message
# message.save!
#
# assert_text message.content
# end
#
# By default, calls to `#visit` will wait for all `<turbo-cable-stream-source>`
# elements to connect. You can control this by modifying the
# `config.turbo.waiting_capybara_actions`. For example, to wait after calls to
# `#click_link`, add the following to `config/environments/test.rb`:
#
# # config/environments/test.rb
# config.turbo.waiting_capybara_actions << :click_link
#
# To disable automatic waiting, set the configuration to `[]`:
#
# # config/environments/test.rb
# config.turbo.waiting_capybara_actions = []
#
def wait_for_turbo_cable_stream_sources(**options)
all(:css, "turbo-cable-stream-source:not([connected])", wait: 0).each do |element|
element.assert_matches_selector(:css, "[connected]", visible: :all, **options)
end
end
end
24 changes: 4 additions & 20 deletions test/system/broadcasts_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ class BroadcastsTest < ApplicationSystemTestCase

test "Message broadcasts Turbo Streams" do
visit messages_path
wait_for_stream_to_be_connected

assert_broadcasts_text "Message 1", to: :messages do |text, target|
Message.create(content: text).broadcast_append_to(target)
Expand All @@ -14,7 +13,6 @@ class BroadcastsTest < ApplicationSystemTestCase

test "Message broadcasts with html: render option" do
visit messages_path
wait_for_stream_to_be_connected

assert_broadcasts_text "Hello, with html: option", to: :messages do |text, target|
Message.create(content: "Ignored").broadcast_append_to(target, html: text)
Expand All @@ -23,25 +21,22 @@ class BroadcastsTest < ApplicationSystemTestCase

test "Message broadcasts with renderable: render option" do
visit messages_path
wait_for_stream_to_be_connected


assert_broadcasts_text "Test message", to: :messages do |text, target|
Message.create(content: "Ignored").broadcast_append_to(target, renderable: MessageComponent.new(text))
end
end

test "Does not render the layout twice when passed a component" do
visit messages_path
wait_for_stream_to_be_connected


Message.create(content: "Ignored").broadcast_append_to(:messages, renderable: MessageComponent.new("test"))

assert_selector("title", count: 1, visible: false, text: "Dummy")
end

test "Message broadcasts with extra attributes to turbo stream tag" do
visit messages_path
wait_for_stream_to_be_connected

assert_broadcasts_text "Message 1", to: :messages do |text, target|
Message.create(content: text).broadcast_action_to(target, action: :append, attributes: { "data-foo": "bar" })
Expand All @@ -50,7 +45,6 @@ class BroadcastsTest < ApplicationSystemTestCase

test "Message broadcasts with correct extra attributes to turbo stream tag" do
visit messages_path
wait_for_stream_to_be_connected

assert_forwards_turbo_stream_tag_attribute attr_key: "data-foo", attr_value: "bar", to: :messages do |attr_key, attr_value, target|
Message.create(content: text).broadcast_action_to(target, action: :test, attributes: { attr_key => attr_value })
Expand All @@ -59,7 +53,6 @@ class BroadcastsTest < ApplicationSystemTestCase

test "Message broadcasts with no rendering" do
visit messages_path
wait_for_stream_to_be_connected

assert_forwards_turbo_stream_tag_attribute attr_key: "data-foo", attr_value: "bar", to: :messages do |attr_key, attr_value, target|
Message.create(content: text).broadcast_action_to(target, action: :test, render: false, partial: "non_existant", attributes: { attr_key => attr_value })
Expand All @@ -68,7 +61,6 @@ class BroadcastsTest < ApplicationSystemTestCase

test "Message broadcasts later with extra attributes to turbo stream tag" do
visit messages_path
wait_for_stream_to_be_connected

perform_enqueued_jobs do
assert_broadcasts_text "Message 1", to: :messages do |text, target|
Expand All @@ -80,7 +72,6 @@ class BroadcastsTest < ApplicationSystemTestCase

test "Message broadcasts later with correct extra attributes to turbo stream tag" do
visit messages_path
wait_for_stream_to_be_connected

perform_enqueued_jobs do
assert_forwards_turbo_stream_tag_attribute attr_key: "data-foo", attr_value: "bar", to: :messages do |attr_key, attr_value, target|
Expand All @@ -91,7 +82,6 @@ class BroadcastsTest < ApplicationSystemTestCase

test "Message broadcasts later with no rendering" do
visit messages_path
wait_for_stream_to_be_connected

perform_enqueued_jobs do
assert_forwards_turbo_stream_tag_attribute attr_key: "data-foo", attr_value: "bar", to: :messages do |attr_key, attr_value, target|
Expand All @@ -102,7 +92,6 @@ class BroadcastsTest < ApplicationSystemTestCase

test "Users::Profile broadcasts Turbo Streams" do
visit users_profiles_path
wait_for_stream_to_be_connected

assert_broadcasts_text "Profile 1", to: :users_profiles do |text, target|
Users::Profile.new(id: 1, name: text).broadcast_append_to(target)
Expand All @@ -111,7 +100,6 @@ class BroadcastsTest < ApplicationSystemTestCase

test "passing extra parameters to channel" do
visit section_messages_path
wait_for_stream_to_be_connected

assert_broadcasts_text "In a section", to: :messages do |text|
Message.create(content: text).broadcast_append_to(:important_messages)
Expand All @@ -120,10 +108,6 @@ class BroadcastsTest < ApplicationSystemTestCase

private

def wait_for_stream_to_be_connected
assert_selector "turbo-cable-stream-source[connected]", visible: false
end

def assert_broadcasts_text(text, to:, &block)
within(:element, id: to) { assert_no_text text }

Expand Down

0 comments on commit f9c07f7

Please sign in to comment.