Skip to content

Commit b3dcc58

Browse files
ihabadhamclaude
andcommitted
Add test coverage for producer early termination on client disconnect
Test that response.stream.closed? check in process_stream_chunks actually stops processing when client disconnects early. Also adds required response mocks to existing streaming tests. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 47ca8b1 commit b3dcc58

File tree

1 file changed

+62
-2
lines changed

1 file changed

+62
-2
lines changed

react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,9 @@ def mock_request_and_response(mock_chunks = chunks, count: 1)
362362
end
363363
end
364364

365-
describe "#stream_react_component" do
365+
describe "#stream_react_component" do # rubocop:disable RSpec/MultipleMemoizedHelpers
366+
let(:mocked_rails_stream) { instance_double(ActionController::Live::Buffer) }
367+
366368
around do |example|
367369
# Wrap each test in Sync block to provide async context
368370
Sync do
@@ -378,6 +380,14 @@ def mock_request_and_response(mock_chunks = chunks, count: 1)
378380
end
379381
end
380382

383+
before do
384+
# Mock response.stream.closed? for client disconnect detection
385+
allow(mocked_rails_stream).to receive(:closed?).and_return(false)
386+
mocked_rails_response = instance_double(ActionDispatch::Response)
387+
allow(mocked_rails_response).to receive(:stream).and_return(mocked_rails_stream)
388+
allow(self).to receive(:response).and_return(mocked_rails_response)
389+
end
390+
381391
it "returns the component shell that exist in the initial chunk with the consoleReplayScript" do
382392
mock_request_and_response
383393
initial_result = stream_react_component(component_name, props: props, **component_options)
@@ -452,6 +462,38 @@ def mock_request_and_response(mock_chunks = chunks, count: 1)
452462
expect(collected_chunks[1]).to include(chunks_with_whitespaces[2][:html])
453463
expect(collected_chunks[2]).to include(chunks_with_whitespaces[3][:html])
454464
end
465+
466+
it "stops processing chunks when client disconnects" do
467+
many_chunks = Array.new(10) do |i|
468+
{ html: "<div>Chunk #{i}</div>", consoleReplayScript: "" }
469+
end
470+
mock_request_and_response(many_chunks)
471+
472+
# Simulate client disconnect after first chunk
473+
call_count = 0
474+
allow(mocked_rails_stream).to receive(:closed?) do
475+
call_count += 1
476+
call_count > 1 # false for first call, true after
477+
end
478+
479+
# Start streaming - first chunk returned synchronously
480+
initial_result = stream_react_component(component_name, props: props, **component_options)
481+
expect(initial_result).to include("<div>Chunk 0</div>")
482+
483+
# Wait for async task to complete
484+
@async_barrier.wait
485+
@main_output_queue.close
486+
487+
# Collect chunks that were enqueued to output
488+
collected_chunks = []
489+
while (chunk = @main_output_queue.dequeue)
490+
collected_chunks << chunk
491+
end
492+
493+
# Should have stopped early - not all chunks processed
494+
# The exact count depends on timing, but should be less than 9 (all remaining)
495+
expect(collected_chunks.length).to be < 9
496+
end
455497
end
456498

457499
describe "stream_view_containing_react_components" do # rubocop:disable RSpec/MultipleMemoizedHelpers
@@ -476,6 +518,7 @@ def mock_request_and_response(mock_chunks = chunks, count: 1)
476518
written_chunks << chunk
477519
end
478520
allow(mocked_stream).to receive(:close)
521+
allow(mocked_stream).to receive(:closed?).and_return(false)
479522
mocked_response = instance_double(ActionDispatch::Response)
480523
allow(mocked_response).to receive(:stream).and_return(mocked_stream)
481524
allow(self).to receive(:response).and_return(mocked_response)
@@ -565,6 +608,7 @@ def execute_stream_view_containing_react_components
565608
written_chunks.clear
566609
allow(mocked_stream).to receive(:write) { |chunk| written_chunks << chunk }
567610
allow(mocked_stream).to receive(:close)
611+
allow(mocked_stream).to receive(:closed?).and_return(false)
568612
mocked_response = instance_double(ActionDispatch::Response)
569613
allow(mocked_response).to receive(:stream).and_return(mocked_stream)
570614
allow(self).to receive(:response).and_return(mocked_response)
@@ -709,7 +753,9 @@ def run_stream
709753
end
710754
end
711755

712-
describe "cached_stream_react_component integration with RandomValue", :caching do
756+
describe "cached_stream_react_component integration with RandomValue", :caching do # rubocop:disable RSpec/MultipleMemoizedHelpers
757+
let(:mocked_stream) { instance_double(ActionController::Live::Buffer) }
758+
713759
around do |example|
714760
original_prerender_caching = ReactOnRailsPro.configuration.prerender_caching
715761
ReactOnRailsPro.configuration.prerender_caching = true
@@ -720,6 +766,13 @@ def run_stream
720766
Rails.cache.clear
721767
end
722768

769+
before do
770+
allow(mocked_stream).to receive(:closed?).and_return(false)
771+
mocked_response = instance_double(ActionDispatch::Response)
772+
allow(mocked_response).to receive(:stream).and_return(mocked_stream)
773+
allow(self).to receive(:response).and_return(mocked_response)
774+
end
775+
723776
# we need this setup because we can't use the helper outside of stream_view_containing_react_components
724777
def render_cached_random_value(cache_key)
725778
# Streaming helpers require this context normally provided by stream_view_containing_react_components
@@ -780,6 +833,7 @@ def render_cached_random_value(cache_key)
780833
{ html: "<div>Test Content</div>", consoleReplayScript: "" }
781834
]
782835
end
836+
let(:mocked_stream) { instance_double(ActionController::Live::Buffer) }
783837

784838
around do |example|
785839
Sync do
@@ -790,6 +844,12 @@ def render_cached_random_value(cache_key)
790844
end
791845

792846
before do
847+
# Mock response.stream.closed? for client disconnect detection
848+
allow(mocked_stream).to receive(:closed?).and_return(false)
849+
mocked_response = instance_double(ActionDispatch::Response)
850+
allow(mocked_response).to receive(:stream).and_return(mocked_stream)
851+
allow(self).to receive(:response).and_return(mocked_response)
852+
793853
ReactOnRailsPro::Request.instance_variable_set(:@connection, nil)
794854
original_httpx_plugin = HTTPX.method(:plugin)
795855
allow(HTTPX).to receive(:plugin) do |*args|

0 commit comments

Comments
 (0)