diff --git a/Gemfile b/Gemfile index 86fa6d96..4e73e34a 100644 --- a/Gemfile +++ b/Gemfile @@ -23,6 +23,7 @@ end group :test do gem 'capybara' + gem 'capybara_accessible_selectors', github: 'citizensadvice/capybara_accessible_selectors', branch: 'main' gem 'rexml' gem 'selenium-webdriver' gem 'webdrivers' diff --git a/app/controllers/turbo/streams/redirect.rb b/app/controllers/turbo/streams/redirect.rb new file mode 100644 index 00000000..b0d52397 --- /dev/null +++ b/app/controllers/turbo/streams/redirect.rb @@ -0,0 +1,24 @@ +module Turbo::Streams::Redirect + extend ActiveSupport::Concern + + def redirect_to(options = {}, response_options = {}) + turbo_frame = response_options.delete(:turbo_frame) + location = url_for(options) + + if request.format.turbo_stream? && turbo_frame.present? + alert, notice, flash_override = response_options.values_at(:alert, :notice, :flash) + flash.merge!(flash_override || {alert: alert, notice: notice}) + + case Rack::Utils.status_code(response_options.fetch(:status, :created)) + when 300..399 then response_options[:status] = :created + end + + render "turbo/streams/redirect", response_options.with_defaults( + locals: {location: location, turbo_frame: turbo_frame}, + location: location, + ) + else + super + end + end +end diff --git a/app/views/application/turbo/streams/redirect.turbo_stream.erb b/app/views/application/turbo/streams/redirect.turbo_stream.erb new file mode 100644 index 00000000..530e4782 --- /dev/null +++ b/app/views/application/turbo/streams/redirect.turbo_stream.erb @@ -0,0 +1,6 @@ +<%= turbo_stream.append_all "head" do %> + <%= javascript_tag nonce: true do %> + window.Turbo.visit("<%= escape_javascript response.location %>", <%= {frame: turbo_frame}.to_json %>) + document.currentScript.remove() + <% end %> +<% end %> diff --git a/lib/turbo/engine.rb b/lib/turbo/engine.rb index 9234f563..49fc3923 100644 --- a/lib/turbo/engine.rb +++ b/lib/turbo/engine.rb @@ -36,7 +36,9 @@ class Engine < Rails::Engine initializer "turbo.helpers", before: :load_config_initializers do ActiveSupport.on_load(:action_controller_base) do - include Turbo::Streams::TurboStreamsTagBuilder, Turbo::Frames::FrameRequest, Turbo::Native::Navigation + include Turbo::Streams::TurboStreamsTagBuilder, Turbo::Frames::FrameRequest, Turbo::Native::Navigation, + Turbo::Streams::Redirect + helper Turbo::Engine.helpers end end diff --git a/test/dummy/app/controllers/articles_controller.rb b/test/dummy/app/controllers/articles_controller.rb index 1810e8e5..22510b33 100644 --- a/test/dummy/app/controllers/articles_controller.rb +++ b/test/dummy/app/controllers/articles_controller.rb @@ -17,12 +17,20 @@ def update redirect_to articles_url end - def create + def new @article = Article.new + end - @article.update! article_params + def create + @article = Article.new article_params - redirect_to articles_url + if @article.save + flash.notice = "Created!" + + redirect_to articles_url, **redirect_params.to_h + else + render :new, status: :unprocessable_entity + end end def destroy @@ -39,6 +47,10 @@ def article_params params.require(:article).permit(:body) end + def redirect_params + params.permit(:alert, :notice, :status, :turbo_frame, flash: {}).with_defaults(status: :found) + end + def assert_param_method! raise unless params[:_method].present? end diff --git a/test/dummy/app/views/articles/new.html.erb b/test/dummy/app/views/articles/new.html.erb new file mode 100644 index 00000000..e8602f06 --- /dev/null +++ b/test/dummy/app/views/articles/new.html.erb @@ -0,0 +1,17 @@ +
+ New Article + + <%= turbo_frame_tag @article do %> + <%= form_with model: @article do |form| %> + <%= form.label :body %> + <%= form.text_area :body, aria: { describedby: (dom_id(@article, :errors) if @article.errors[:body].any?) } %> + <% if @article.errors[:body].any? %> +

+ <%= @article.errors[:body].to_sentence %> +

+ <% end %> + + <%= form.button name: :turbo_frame, value: "_top"%> + <% end %> + <% end %> +
diff --git a/test/streams/redirect_test.rb b/test/streams/redirect_test.rb new file mode 100644 index 00000000..6d981cc3 --- /dev/null +++ b/test/streams/redirect_test.rb @@ -0,0 +1,124 @@ +require "test_helper" + +class Turbo::Streams::RedirectTest < ActionDispatch::IntegrationTest + test "html requests respond with a redirect HTTP status" do + post articles_path, params: { + turbo_frame: "_top", status: 303, + article: {body: "A valid value"} + } + + assert_response :see_other + assert_redirected_to articles_url + assert_equal "Created!", flash[:notice] + end + + test "html redirects write to the flash" do + post articles_path, params: { + turbo_frame: "_top", flash: {alert: "Wrote to alert:"}, + article: {body: "A valid value"} + } + + assert_equal "Wrote to alert:", flash[:alert] + end + + test "html redirects write to alert" do + post articles_path, params: { + turbo_frame: "_top", alert: "Wrote to alert:", + article: {body: "A valid value"} + } + + assert_equal "Wrote to alert:", flash[:alert] + end + + test "html redirects write to notice" do + post articles_path, params: { + turbo_frame: "_top", notice: "Wrote to notice:", + article: {body: "A valid value"} + } + + assert_equal "Wrote to notice:", flash[:notice] + end + + test "turbo_stream requests with the turbo_frame: option responds with a redirect Turbo Stream" do + post articles_path, as: :turbo_stream, params: { + turbo_frame: "_top", + article: {body: "A valid value"} + } + + assert_turbo_stream action: :append, targets: "head", status: :created do + assert_select "script", count: 1 + end + assert_equal articles_url, response.location + end + + test "turbo_stream requests with the turbo_frame: option preserves status: values in the 2xx range" do + post articles_path, as: :turbo_stream, params: { + turbo_frame: "_top", status: 200, + article: { body: "A valid value" } + } + + assert_response 200 + end + + test "turbo_stream requests with the turbo_frame: option replaces status: values in the 3xx range with 201 Created" do + post articles_path, as: :turbo_stream, params: { + turbo_frame: "_top", status: 303, + article: { body: "A valid value" } + } + + assert_response 201 + end + + test "turbo_stream requests with the turbo_frame: option preserves status: values in the 4xx range" do + post articles_path, as: :turbo_stream, params: { + turbo_frame: "_top", status: 403, + article: { body: "A valid value" } + } + + assert_response 403 + end + + test "turbo_stream requests with the turbo_frame: option preserves status: values in the 5xx range" do + post articles_path, as: :turbo_stream, params: { + turbo_frame: "_top", status: 500, + article: { body: "A valid value" } + } + + assert_response 500 + end + + test "turbo_stream requests without the turbo_frame: option respond with a redirect HTTP status" do + post articles_path, as: :turbo_stream, params: { + article: { body: "A valid value" } + } + + assert_redirected_to articles_url + end + + test "turbo_stream redirects write to the flash" do + post articles_path, as: :turbo_stream, params: { + turbo_frame: "_top", flash: {alert: "Wrote to alert:"}, + article: {body: "A valid value"} + } + + assert_equal "Wrote to alert:", flash[:alert] + end + + test "turbo_stream redirects write to alert" do + post articles_path, as: :turbo_stream, params: { + turbo_frame: "_top", alert: "Wrote to alert:", + article: {body: "A valid value"} + } + + assert_equal "Wrote to alert:", flash[:alert] + end + + test "turbo_stream redirects write to notice" do + post articles_path, as: :turbo_stream, params: { + turbo_frame: "_top", notice: "Wrote to notice:", + article: {body: "A valid value"} + } + + assert_equal "Wrote to notice:", flash[:notice] + end +end diff --git a/test/system/frames_test.rb b/test/system/frames_test.rb new file mode 100644 index 00000000..ed723021 --- /dev/null +++ b/test/system/frames_test.rb @@ -0,0 +1,24 @@ +require "application_system_test_case" + +class FramesTest < ApplicationSystemTestCase + test "can render an invalid submission within a frame" do + visit new_article_path + toggle_disclosure "New Article" + click_on "Create Article" + + within_disclosure "New Article" do + assert_field "Body", described_by: "can't be blank" + end + end + + test "can redirect the entire page after a valid submission within a frame" do + visit new_article_path + toggle_disclosure "New Article" + fill_in "Body", with: "An article's body" + click_on "Create Article" + + assert_no_selector :disclosure, "New Article" + assert_no_field "Body" + assert_text "An article's body" + end +end