ruby erb 86 lines · 3 tabs

Request spec: Turbo Stream template is rendered

Shared by codesnips Jan 2026
3 tabs
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

    respond_to do |format|
      if @comment.save
        format.turbo_stream
        format.html { redirect_to @post, notice: "Comment added." }
      else
        format.turbo_stream do
          render turbo_stream: turbo_stream.replace(
            "new_comment",
            partial: "comments/form",
            locals: { post: @post, comment: @comment }
          ), status: :unprocessable_entity
        end
        format.html { render "posts/show", status: :unprocessable_entity }
      end
    end
  end

  private

  def set_post
    @post = Post.find(params[:post_id])
  end

  def comment_params
    params.require(:comment).permit(:body)
  end
end
3 files · ruby, erb Explain with highlit

Turbo Streams let a Rails controller respond to a form submission with fragment updates instead of a full page reload, but that behavior is easy to break silently: a wrong target, a stale partial, or a missing format.turbo_stream branch will simply fall back to HTML without any error. This snippet shows how to lock that behavior down with a request spec that asserts on the actual text/vnd.turbo-stream.html payload.

The CommentsController in the first tab is the subject under test. Its create action saves a comment and then uses respond_to so the same endpoint serves both classic HTML and Turbo. The format.turbo_stream branch renders create.turbo_stream.erb, while format.html keeps a working non-JS fallback via redirect_to. Writing the controller this way means progressive enhancement is real, not aspirational — the HTML path still works if JavaScript is disabled.

The create.turbo_stream.erb template in the second tab is where the DOM diff is declared. turbo_stream.append targets the DOM id comments and renders the comments/comment partial, while a second turbo_stream.update swaps the new_comment form container to reset it. Each action serializes to a <turbo-stream> element that the Turbo runtime applies on the client. These target ids are a contract, so the test needs to verify them explicitly.

The comments request spec in the third tab drives the endpoint with post and sets the Accept header so Rails picks the Turbo variant. The response Content-Type is asserted to be text/vnd.turbo-stream.html, which confirms the correct respond_to branch fired. Because the body is XML-ish markup, the spec parses it with Capybara.string and uses have_selector with the turbo-stream element and its action and target attributes — checking behavior rather than brittle string matching.

The key pitfall this guards against is the invisible HTML fallback: without the Accept header the request returns a redirect and the assertions on target never run. A second example covers validation failure, asserting the stream re-renders the form with errors rather than appending an invalid comment. Testing at the request level keeps these specs fast and free of a browser while still exercising the real routing, respond_to, and template layers where Turbo bugs actually live.


Related snips

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
class Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy

  scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
  scope :draft, -> { where(published_at: nil) }

ActiveRecord scopes for reusable query logic

rails activerecord patterns
by Alex Kumar 1 tab
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
erb
<form data-controller="query-sync" data-action="change->query-sync#apply">
  <select name="status" class="rounded border p-2">
    <option value="">Any</option>
    <option value="open">Open</option>
    <option value="closed">Closed</option>
  </select>

Filter UI that syncs query params via Stimulus (no front-end router)

rails hotwire stimulus
by Henry Kim 2 tabs

Share this code

Here's the card — post it anywhere.

Request spec: Turbo Stream template is rendered — share card
Link copied