1

I'm writing a request test in RSpec and in the course of the controller path I am testing, the application sends out two turbo stream broadcasts.

I have not been able to get the broadcast_to expectation to work in this case. I feel I am close, but I have hit a wall and thought I'd ask a question while continuing to do research.

Context

The models in question are a press release, which has many press release image models. Each press release image model has one attached image.

The controller action takes a file attachment and attaches it to the press release model, then broadcasts an update targeting a frame:

image_model.broadcast_replace_to(image_model.press_release, target: "slot_#{image_model.position}"

The reason we do image_model.press_release to pass in *streamable is because our view starts a Turbo stream via turbo_stream_from(@press_release) so we need to target the right stream.

Problem

I have this expectation and it is just not hitting right:

expect do
  post press_release_images_path(press_release), params: params,
                                                 headers: { 'Content-Type' => 'multipart/form-data' }
end.to broadcast_to(press_release).from_channel(Turbo::StreamsChannel)

This snippet seems to at least let the test run, but it fails as there are 0 broadcasts to this channel. What's curious though, is that in my test logs when running this test, I see a similiar stream name.

Failure messsage from above expect block:
expected to broadcast exactly 1 messages to turbo:streams:Z2lkOi8vcHJlc3MtcmVsZWFzZS1nZW5lcmF0b3IvUHJlc3NSZWxlYXNlLzE5NDM, but broadcast 0

Logs of test controller action:
Rendered press_releases/images/_display.html.erb (Duration: 15.2ms | Allocations: 9700) [ActionCable] Broadcasting to Z2lkOi8vcHJlc3MtcmVsZWFzZS1nZW5lcmF0b3IvUHJlc3NSZWxlYXNlLzE5NDM: "<turbo-stream action=\"update\"...

There is a second broadcast as well, but the same issue persists: I seemingly have the overall direction correct, but can't seem to figure out how to match the stream name. The generated strings are present in both the RSpec error message and the testlogs, but I am not sure how to reconcile them to get the test passing.

While I would be keen to solve the problem along the same shape that I've outlined here, I am open to alternative approaches to testing this behaviour!

crespire
  • 33
  • 1
  • 7

2 Answers2

1

Quick fix:

expect do
  post press_release_images_path(press_release), params: params, headers: {"Content-Type" => "multipart/form-data"}
end.to(
  broadcast_to(press_release.to_gid_param)
)

This name looked a little off:

turbo:streams:Z2lkOi8vcHJlc3MtcmVsZWFzZS1nZW5lcmF0b3IvUHJlc3NSZWxlYXNlLzE5NDM

It is generated like this @channel.broadcasting_for(@target)

https://github.com/rspec/rspec-rails/blob/v6.0.3/lib/rspec/rails/matchers/action_cable/have_broadcasted_to.rb#L106

>> Turbo::StreamsChannel.broadcasting_for(Model.first)
=> "turbo:streams:Z2lkOi8vc3RhY2tvdmVyZmxvdy9Nb2RlbC8x"

But you're generating stream name in your views:

>> puts helper.turbo_stream_from(Model.first)
# <turbo-cable-stream-source channel="Turbo::StreamsChannel" signed-stream-name="IloybGtPaTh2YzNSaFkydHZkbVZ5Wm14dmR5OU5iMlJsYkM4eCI=--422c4102fe87e2e8e48bd0ed5238039276c665c95b330bde1924f3783571ab78"></turbo-cable-stream-source>

# where signed stream name is
>> Turbo::StreamsChannel.signed_stream_name(Model.first)
=> "IloybGtPaTh2YzNSaFkydHZkbVZ5Wm14dmR5OU5iMlJsYkM4eCI=--422c4102fe87e2e8e48bd0ed5238039276c665c95b330bde1924f3783571ab78"

# which can be decrypted
>> Turbo.signed_stream_verifier.verified(Turbo::StreamsChannel.signed_stream_name(Model.first))
=> "Z2lkOi8vc3RhY2tvdmVyZmxvdy9Nb2RlbC8x"
# and that's what you see in the logs

Note turbo:streams: is not there. Here is how Turbo gets that name:

https://github.com/hotwired/turbo-rails/blob/v1.4.0/app/channels/turbo/streams/stream_name.rb#L28

>> Model.first.to_gid_param  # or .to_gid.to_param
=> "Z2lkOi8vc3RhY2tvdmVyZmxvdy9Nb2RlbC8x"

# which is just a global id that you can use to find the model
>> GlobalID::Locator.locate "Z2lkOi8vc3RhY2tvdmVyZmxvdy9Nb2RlbC8x"
=> #<Model:0x00007f9241cf4b00 id: 1>

https://github.com/rails/globalid

You can pass stream name as a string to broadcast_to("Z2lkOi8vc3RhY2tvdmVyZmxvdy9Nb2RlbC8x").

Alex
  • 16,409
  • 6
  • 40
  • 56
0

I ended up going the mock route, even though it isn't as targeted. We basically just set up an expectation that the broadcast methods are called on the image_model instance.

It's not really side effect testing, but it gets to the point of the test:

expect_any_instance_of(PressReleaseImage).to receive(:broadcast_replace_to).once
expect_any_instance_of(PressReleaseImage).to receive(:broadcast_update_to).once
expect do
  post press_release_images_path(press_release), params: params, headers: { 'Content-Type' => 'multipart/form-data' }
end.to change { press_release.reload.images.size }.from(0).to(1)

Given that Turbo is well tested library code, checking that the method is sent gives me enough assurance that things will work.

crespire
  • 33
  • 1
  • 7