turbo-rails 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -23,6 +23,8 @@ module Turbo::Frames::FrameRequest
23
23
  included do
24
24
  layout -> { "turbo_rails/frame" if turbo_frame_request? }
25
25
  etag { :frame if turbo_frame_request? }
26
+
27
+ helper_method :turbo_frame_request_id
26
28
  end
27
29
 
28
30
  private
@@ -2,23 +2,34 @@
2
2
  # have Turbo Native clients running (see the Turbo iOS and Turbo Android projects for details), you can respond to native
3
3
  # requests with three dedicated responses: <tt>recede</tt>, <tt>resume</tt>, <tt>refresh</tt>.
4
4
  #
5
- # FIXME: Supply full description of when we use either.
5
+ # turbo-android handles these actions automatically. You are required to implement the handling on your own for turbo-ios.
6
6
  module Turbo::Native::Navigation
7
- private
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ helper_method :turbo_native_app?
11
+ end
8
12
 
13
+ # Turbo Native applications are identified by having the string "Turbo Native" as part of their user agent.
14
+ def turbo_native_app?
15
+ request.user_agent.to_s.match?(/Turbo Native/)
16
+ end
17
+
18
+ # Tell the Turbo Native app to dismiss a modal (if presented) or pop a screen off of the navigation stack.
9
19
  def recede_or_redirect_to(url, **options)
10
20
  turbo_native_action_or_redirect url, :recede, :to, options
11
21
  end
12
22
 
23
+ # Tell the Turbo Native app to ignore this navigation.
13
24
  def resume_or_redirect_to(url, **options)
14
25
  turbo_native_action_or_redirect url, :resume, :to, options
15
26
  end
16
27
 
28
+ # Tell the Turbo Native app to refresh the current screen.
17
29
  def refresh_or_redirect_to(url, **options)
18
30
  turbo_native_action_or_redirect url, :refresh, :to, options
19
31
  end
20
32
 
21
-
22
33
  def recede_or_redirect_back_or_to(url, **options)
23
34
  turbo_native_action_or_redirect url, :recede, :back, options
24
35
  end
@@ -30,6 +41,8 @@ module Turbo::Native::Navigation
30
41
  def refresh_or_redirect_back_or_to(url, **options)
31
42
  turbo_native_action_or_redirect url, :refresh, :back, options
32
43
  end
44
+
45
+ private
33
46
 
34
47
  # :nodoc:
35
48
  def turbo_native_action_or_redirect(url, action, redirect_type, options = {})
@@ -43,9 +56,4 @@ module Turbo::Native::Navigation
43
56
  redirect_to url, options
44
57
  end
45
58
  end
46
-
47
- # Turbo Native applications are identified by having the string "Turbo Native" as part of their user agent.
48
- def turbo_native_app?
49
- request.user_agent.to_s.match?(/Turbo Native/)
50
- end
51
59
  end
@@ -36,7 +36,7 @@ module Turbo::FramesHelper
36
36
  # <%= turbo_frame_tag(Article.find(1), Comment.new) %>
37
37
  # # => <turbo-frame id="article_1_new_comment"></turbo-frame>
38
38
  def turbo_frame_tag(*ids, src: nil, target: nil, **attributes, &block)
39
- id = ids.map { |id| id.respond_to?(:to_key) ? ActionView::RecordIdentifier.dom_id(id) : id }.join("_")
39
+ id = ids.first.respond_to?(:to_key) ? ActionView::RecordIdentifier.dom_id(*ids) : ids.first
40
40
  src = url_for(src) if src.present?
41
41
 
42
42
  tag.turbo_frame(**attributes.merge(id: id, src: src, target: target).compact, &block)
@@ -11,6 +11,18 @@ module Turbo::Streams::ActionHelper
11
11
  #
12
12
  # turbo_stream_action_tag "replace", targets: "message_1", template: %(<div id="message_1">Hello!</div>)
13
13
  # # => <turbo-stream action="replace" targets="message_1"><template><div id="message_1">Hello!</div></template></turbo-stream>
14
+ #
15
+ # The `target:` keyword option will forward `ActionView::RecordIdentifier#dom_id`-compatible arguments to
16
+ # `ActionView::RecordIdentifier#dom_id`
17
+ #
18
+ # message = Message.find(1)
19
+ # turbo_stream_action_tag "remove", target: message
20
+ # # => <turbo-stream action="remove" target="message_1"></turbo-stream>
21
+ #
22
+ # message = Message.find(1)
23
+ # turbo_stream_action_tag "remove", target: [message, :special]
24
+ # # => <turbo-stream action="remove" target="special_message_1"></turbo-stream>
25
+ #
14
26
  def turbo_stream_action_tag(action, target: nil, targets: nil, template: nil, **attributes)
15
27
  template = action.to_sym == :remove ? "" : tag.template(template.to_s.html_safe)
16
28
 
@@ -25,8 +37,8 @@ module Turbo::Streams::ActionHelper
25
37
 
26
38
  private
27
39
  def convert_to_turbo_stream_dom_id(target, include_selector: false)
28
- if target.respond_to?(:to_key)
29
- "#{"#" if include_selector}#{ActionView::RecordIdentifier.dom_id(target)}"
40
+ if Array(target).any? { |value| value.respond_to?(:to_key) }
41
+ "#{"#" if include_selector}#{ActionView::RecordIdentifier.dom_id(*target)}"
30
42
  else
31
43
  target
32
44
  end
@@ -35,7 +35,16 @@ function determineFetchMethod(submitter, body, form) {
35
35
 
36
36
  function determineFormMethod(submitter) {
37
37
  if (submitter instanceof HTMLButtonElement || submitter instanceof HTMLInputElement) {
38
- if (submitter.hasAttribute("formmethod")) {
38
+ // Rails 7 ActionView::Helpers::FormBuilder#button method has an override
39
+ // for formmethod if the button does not have name or value attributes
40
+ // set, which is the default. This means that if you use <%= f.button
41
+ // formmethod: :delete %>, it will generate a <button name="_method"
42
+ // value="delete" formmethod="post">. Therefore, if the submitter's name
43
+ // is already _method, it's value attribute already contains the desired
44
+ // method.
45
+ if (submitter.name === '_method') {
46
+ return submitter.value
47
+ } else if (submitter.hasAttribute("formmethod")) {
39
48
  return submitter.formMethod
40
49
  } else {
41
50
  return null
@@ -54,6 +54,19 @@
54
54
  # end
55
55
  # end
56
56
  #
57
+ # If you want to render a renderable object you can use the `renderable:` option.
58
+ #
59
+ # class Message < ApplicationRecord
60
+ # belongs_to :user
61
+ #
62
+ # after_create_commit :update_message
63
+ #
64
+ # private
65
+ # def update_message
66
+ # broadcast_replace_to(user, :message, target: "message", renderable: MessageComponent.new)
67
+ # end
68
+ # end
69
+ #
57
70
  # There are four basic actions you can broadcast: <tt>remove</tt>, <tt>replace</tt>, <tt>append</tt>, and
58
71
  # <tt>prepend</tt>. As a rule, you should use the <tt>_later</tt> versions of everything except for remove when broadcasting
59
72
  # within a real-time path, like a controller or model, since all those updates require a rendering step, which can slow down
@@ -352,7 +365,7 @@ module Turbo::Broadcastable
352
365
 
353
366
  if o[:html] || o[:partial]
354
367
  return o
355
- elsif o[:template]
368
+ elsif o[:template] || o[:renderable]
356
369
  o[:layout] = false
357
370
  else
358
371
  # if none of these options are passed in, it will set a partial from #to_partial_path
data/config/routes.rb CHANGED
@@ -3,4 +3,4 @@ Rails.application.routes.draw do
3
3
  get "recede_historical_location" => "turbo/native/navigation#recede", as: :turbo_recede_historical_location
4
4
  get "resume_historical_location" => "turbo/native/navigation#resume", as: :turbo_resume_historical_location
5
5
  get "refresh_historical_location" => "turbo/native/navigation#refresh", as: :turbo_refresh_historical_location
6
- end
6
+ end if Turbo.draw_routes
@@ -0,0 +1,9 @@
1
+ if (js_entrypoint_path = Rails.root.join("app/javascript/application.js")).exist?
2
+ say "Import Turbo"
3
+ append_to_file "app/javascript/application.js", %(import "@hotwired/turbo-rails"\n)
4
+ else
5
+ say "You must import @hotwired/turbo-rails in your JavaScript entrypoint file", :red
6
+ end
7
+
8
+ say "Install Turbo"
9
+ run "bun add @hotwired/turbo-rails"
@@ -1,18 +1,27 @@
1
- def run_turbo_install_template(path)
2
- system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{File.expand_path("../install/#{path}.rb", __dir__)}"
3
- end
1
+ module Turbo
2
+ module Tasks
3
+ extend self
4
+ def run_turbo_install_template(path)
5
+ system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{File.expand_path("../install/#{path}.rb", __dir__)}"
6
+ end
4
7
 
5
- def redis_installed?
6
- Gem.win_platform? ?
7
- system('where redis-server > NUL 2>&1') :
8
- system('which redis-server > /dev/null')
9
- end
8
+ def redis_installed?
9
+ Gem.win_platform? ?
10
+ system('where redis-server > NUL 2>&1') :
11
+ system('which redis-server > /dev/null')
12
+ end
10
13
 
11
- def switch_on_redis_if_available
12
- if redis_installed?
13
- Rake::Task["turbo:install:redis"].invoke
14
- else
15
- puts "Run turbo:install:redis to switch on Redis and use it in development for turbo streams"
14
+ def switch_on_redis_if_available
15
+ if redis_installed?
16
+ Rake::Task["turbo:install:redis"].invoke
17
+ else
18
+ puts "Run turbo:install:redis to switch on Redis and use it in development for turbo streams"
19
+ end
20
+ end
21
+
22
+ def using_bun?
23
+ Rails.root.join("bun.config.js").exist?
24
+ end
16
25
  end
17
26
  end
18
27
 
@@ -21,6 +30,8 @@ namespace :turbo do
21
30
  task :install do
22
31
  if Rails.root.join("config/importmap.rb").exist?
23
32
  Rake::Task["turbo:install:importmap"].invoke
33
+ elsif Rails.root.join("package.json").exist? && Turbo::Tasks.using_bun?
34
+ Rake::Task["turbo:install:bun"].invoke
24
35
  elsif Rails.root.join("package.json").exist?
25
36
  Rake::Task["turbo:install:node"].invoke
26
37
  else
@@ -31,19 +42,25 @@ namespace :turbo do
31
42
  namespace :install do
32
43
  desc "Install Turbo into the app with asset pipeline"
33
44
  task :importmap do
34
- run_turbo_install_template "turbo_with_importmap"
35
- switch_on_redis_if_available
45
+ Turbo::Tasks.run_turbo_install_template "turbo_with_importmap"
46
+ Turbo::Tasks.switch_on_redis_if_available
36
47
  end
37
48
 
38
49
  desc "Install Turbo into the app with webpacker"
39
50
  task :node do
40
- run_turbo_install_template "turbo_with_node"
41
- switch_on_redis_if_available
51
+ Turbo::Tasks.run_turbo_install_template "turbo_with_node"
52
+ Turbo::Tasks.switch_on_redis_if_available
53
+ end
54
+
55
+ desc "Install Turbo into the app with bun"
56
+ task :bun do
57
+ Turbo::Tasks.run_turbo_install_template "turbo_with_bun"
58
+ Turbo::Tasks.switch_on_redis_if_available
42
59
  end
43
60
 
44
61
  desc "Switch on Redis and use it in development"
45
62
  task :redis do
46
- run_turbo_install_template "turbo_needs_redis"
63
+ Turbo::Tasks.run_turbo_install_template "turbo_needs_redis"
47
64
  end
48
65
  end
49
66
  end
@@ -0,0 +1,172 @@
1
+ module Turbo
2
+ module Broadcastable
3
+ module TestHelper
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ include ActionCable::TestHelper
8
+
9
+ include Turbo::Streams::StreamName
10
+ end
11
+
12
+ # Asserts that `<turbo-stream>` elements were broadcast over Action Cable
13
+ #
14
+ # === Arguments
15
+ #
16
+ # * <tt>stream_name_or_object</tt> the objects used to generate the
17
+ # channel Action Cable name, or the name itself
18
+ # * <tt>&block</tt> optional block executed before the
19
+ # assertion
20
+ #
21
+ # === Options
22
+ #
23
+ # * <tt>count:</tt> the number of `<turbo-stream>` elements that are
24
+ # expected to be broadcast
25
+ #
26
+ # Asserts `<turbo-stream>` elements were broadcast:
27
+ #
28
+ # message = Message.find(1)
29
+ # message.broadcast_replace_to "messages"
30
+ #
31
+ # assert_turbo_stream_broadcasts "messages"
32
+ #
33
+ # Asserts that two `<turbo-stream>` elements were broadcast:
34
+ #
35
+ # message = Message.find(1)
36
+ # message.broadcast_replace_to "messages"
37
+ # message.broadcast_remove_to "messages"
38
+ #
39
+ # assert_turbo_stream_broadcasts "messages", count: 2
40
+ #
41
+ # You can pass a block to run before the assertion:
42
+ #
43
+ # message = Message.find(1)
44
+ #
45
+ # assert_turbo_stream_broadcasts "messages" do
46
+ # message.broadcast_append_to "messages"
47
+ # end
48
+ #
49
+ # In addition to a String, the helper also accepts an Object or Array to
50
+ # determine the name of the channel the elements are broadcast to:
51
+ #
52
+ # message = Message.find(1)
53
+ #
54
+ # assert_turbo_stream_broadcasts message do
55
+ # message.broadcast_replace
56
+ # end
57
+ #
58
+ def assert_turbo_stream_broadcasts(stream_name_or_object, count: nil, &block)
59
+ payloads = capture_turbo_stream_broadcasts(stream_name_or_object, &block)
60
+ stream_name = stream_name_from(stream_name_or_object)
61
+
62
+ if count.nil?
63
+ assert_not_empty payloads, "Expected at least one broadcast on #{stream_name.inspect}, but there were none"
64
+ else
65
+ broadcasts = "Turbo Stream broadcast".pluralize(count)
66
+
67
+ assert count == payloads.count, "Expected #{count} #{broadcasts} on #{stream_name.inspect}, but there were none"
68
+ end
69
+ end
70
+
71
+ # Asserts that no `<turbo-stream>` elements were broadcast over Action Cable
72
+ #
73
+ # === Arguments
74
+ #
75
+ # * <tt>stream_name_or_object</tt> the objects used to generate the
76
+ # channel Action Cable name, or the name itself
77
+ # * <tt>&block</tt> optional block executed before the
78
+ # assertion
79
+ #
80
+ # Asserts that no `<turbo-stream>` elements were broadcast:
81
+ #
82
+ # message = Message.find(1)
83
+ # message.broadcast_replace_to "messages"
84
+ #
85
+ # assert_no_turbo_stream_broadcasts "messages" # fails with MiniTest::Assertion error
86
+ #
87
+ # You can pass a block to run before the assertion:
88
+ #
89
+ # message = Message.find(1)
90
+ #
91
+ # assert_no_turbo_stream_broadcasts "messages" do
92
+ # # do something other than broadcast to "messages"
93
+ # end
94
+ #
95
+ # In addition to a String, the helper also accepts an Object or Array to
96
+ # determine the name of the channel the elements are broadcast to:
97
+ #
98
+ # message = Message.find(1)
99
+ #
100
+ # assert_no_turbo_stream_broadcasts message do
101
+ # # do something other than broadcast to "message_1"
102
+ # end
103
+ #
104
+ def assert_no_turbo_stream_broadcasts(stream_name_or_object, &block)
105
+ block&.call
106
+
107
+ stream_name = stream_name_from(stream_name_or_object)
108
+
109
+ payloads = broadcasts(stream_name)
110
+
111
+ assert payloads.empty?, "Expected no broadcasts on #{stream_name.inspect}, but there were #{payloads.count}"
112
+ end
113
+
114
+ # Captures any `<turbo-stream>` elements that were broadcast over Action Cable
115
+ #
116
+ # === Arguments
117
+ #
118
+ # * <tt>stream_name_or_object</tt> the objects used to generate the
119
+ # channel Action Cable name, or the name itself
120
+ # * <tt>&block</tt> optional block to capture broadcasts during execution
121
+ #
122
+ # Returns any `<turbo-stream>` elements that have been broadcast as an
123
+ # Array of <tt>Nokogiri::XML::Element</tt> instances
124
+ #
125
+ # message = Message.find(1)
126
+ # message.broadcast_append_to "messages"
127
+ # message.broadcast_prepend_to "messages"
128
+ #
129
+ # turbo_streams = capture_turbo_stream_broadcasts "messages"
130
+ #
131
+ # assert_equal "append", turbo_streams.first["action"]
132
+ # assert_equal "prepend", turbo_streams.second["action"]
133
+ #
134
+ # You can pass a block to limit the scope of the broadcasts being captured:
135
+ #
136
+ # message = Message.find(1)
137
+ #
138
+ # turbo_streams = capture_turbo_stream_broadcasts "messages" do
139
+ # message.broadcast_append_to "messages"
140
+ # end
141
+ #
142
+ # assert_equal "append", turbo_streams.first["action"]
143
+ #
144
+ # In addition to a String, the helper also accepts an Object or Array to
145
+ # determine the name of the channel the elements are broadcast to:
146
+ #
147
+ # message = Message.find(1)
148
+ #
149
+ # replace, remove = capture_turbo_stream_broadcasts message do
150
+ # message.broadcast_replace
151
+ # message.broadcast_remove
152
+ # end
153
+ #
154
+ # assert_equal "replace", replace["action"]
155
+ # assert_equal "replace", remove["action"]
156
+ #
157
+ def capture_turbo_stream_broadcasts(stream_name_or_object, &block)
158
+ block&.call
159
+
160
+ stream_name = stream_name_from(stream_name_or_object)
161
+ payloads = broadcasts(stream_name)
162
+
163
+ payloads.flat_map do |payload|
164
+ html = ActiveSupport::JSON.decode(payload)
165
+ document = Nokogiri::HTML5.parse(html)
166
+
167
+ document.at("body").element_children
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
data/lib/turbo/engine.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require "rails/engine"
2
- require "turbo/test_assertions"
3
2
 
4
3
  module Turbo
5
4
  class Engine < Rails::Engine
@@ -34,6 +33,12 @@ module Turbo
34
33
  end
35
34
  end
36
35
 
36
+ initializer "turbo.configs" do
37
+ config.after_initialize do |app|
38
+ Turbo.draw_routes = app.config.turbo.draw_routes != false
39
+ end
40
+ end
41
+
37
42
  initializer "turbo.helpers", before: :load_config_initializers do
38
43
  ActiveSupport.on_load(:action_controller_base) do
39
44
  include Turbo::Streams::TurboStreamsTagBuilder, Turbo::Frames::FrameRequest, Turbo::Native::Navigation
@@ -69,8 +74,17 @@ module Turbo
69
74
 
70
75
  initializer "turbo.test_assertions" do
71
76
  ActiveSupport.on_load(:active_support_test_case) do
77
+ require "turbo/test_assertions"
78
+ require "turbo/broadcastable/test_helper"
79
+
72
80
  include Turbo::TestAssertions
73
81
  end
82
+
83
+ ActiveSupport.on_load(:action_dispatch_integration_test) do
84
+ require "turbo/test_assertions/integration_test_assertions"
85
+
86
+ include Turbo::TestAssertions::IntegrationTestAssertions
87
+ end
74
88
  end
75
89
 
76
90
  initializer "turbo.integration_test_request_encoding" do
@@ -0,0 +1,76 @@
1
+ module Turbo
2
+ module TestAssertions
3
+ module IntegrationTestAssertions
4
+ # Assert that the Turbo Stream request's response body's HTML contains a
5
+ # `<turbo-stream>` element.
6
+ #
7
+ # === Options
8
+ #
9
+ # * <tt>:status</tt> [Integer, Symbol] the HTTP response status
10
+ # * <tt>:action</tt> [String] matches the element's <tt>[action]</tt>
11
+ # attribute
12
+ # * <tt>:target</tt> [String, #to_key] matches the element's
13
+ # <tt>[target]</tt> attribute. If the value responds to <tt>#to_key</tt>,
14
+ # the value will be transformed by calling <tt>dom_id</tt>
15
+ # * <tt>:targets</tt> [String] matches the element's <tt>[targets]</tt>
16
+ # attribute
17
+ #
18
+ # Given the following HTML response body:
19
+ #
20
+ # <turbo-stream action="remove" target="message_1"></turbo-stream>
21
+ #
22
+ # The following assertion would pass:
23
+ #
24
+ # assert_turbo_stream action: "remove", target: "message_1"
25
+ #
26
+ # You can also pass a block make assertions about the contents of the
27
+ # element. Given the following HTML response body:
28
+ #
29
+ # <turbo-stream action="replace" target="message_1">
30
+ # <template>
31
+ # <p>Hello!</p>
32
+ # <template>
33
+ # </turbo-stream>
34
+ #
35
+ # The following assertion would pass:
36
+ #
37
+ # assert_turbo_stream action: "replace", target: "message_1" do
38
+ # assert_select "template p", text: "Hello!"
39
+ # end
40
+ #
41
+ def assert_turbo_stream(status: :ok, **attributes, &block)
42
+ assert_response status
43
+ assert_equal Mime[:turbo_stream], response.media_type
44
+ super(**attributes, &block)
45
+ end
46
+
47
+ # Assert that the Turbo Stream request's response body's HTML does not
48
+ # contain a `<turbo-stream>` element.
49
+ #
50
+ # === Options
51
+ #
52
+ # * <tt>:status</tt> [Integer, Symbol] the HTTP response status
53
+ # * <tt>:action</tt> [String] matches the element's <tt>[action]</tt>
54
+ # attribute
55
+ # * <tt>:target</tt> [String, #to_key] matches the element's
56
+ # <tt>[target]</tt> attribute. If the value responds to <tt>#to_key</tt>,
57
+ # the value will be transformed by calling <tt>dom_id</tt>
58
+ # * <tt>:targets</tt> [String] matches the element's <tt>[targets]</tt>
59
+ # attribute
60
+ #
61
+ # Given the following HTML response body:
62
+ #
63
+ # <turbo-stream action="remove" target="message_1"></turbo-stream>
64
+ #
65
+ # The following assertion would fail:
66
+ #
67
+ # assert_no_turbo_stream action: "remove", target: "message_1"
68
+ #
69
+ def assert_no_turbo_stream(status: :ok, **attributes)
70
+ assert_response status
71
+ assert_equal Mime[:turbo_stream], response.media_type
72
+ super(**attributes)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -7,17 +7,73 @@ module Turbo
7
7
  delegate :dom_id, :dom_class, to: ActionView::RecordIdentifier
8
8
  end
9
9
 
10
- def assert_turbo_stream(action:, target: nil, targets: nil, status: :ok, &block)
11
- assert_response status
12
- assert_equal Mime[:turbo_stream], response.media_type
10
+ # Assert that the rendered fragment of HTML contains a `<turbo-stream>`
11
+ # element.
12
+ #
13
+ # === Options
14
+ #
15
+ # * <tt>:action</tt> [String] matches the element's <tt>[action]</tt>
16
+ # attribute
17
+ # * <tt>:target</tt> [String, #to_key] matches the element's
18
+ # <tt>[target]</tt> attribute. If the value responds to <tt>#to_key</tt>,
19
+ # the value will be transformed by calling <tt>dom_id</tt>
20
+ # * <tt>:targets</tt> [String] matches the element's <tt>[targets]</tt>
21
+ # attribute
22
+ # * <tt>:count</tt> [Integer] indicates how many turbo streams are expected.
23
+ # Defaults to <tt>1</tt>.
24
+ #
25
+ # Given the following HTML fragment:
26
+ #
27
+ # <turbo-stream action="remove" target="message_1"></turbo-stream>
28
+ #
29
+ # The following assertion would pass:
30
+ #
31
+ # assert_turbo_stream action: "remove", target: "message_1"
32
+ #
33
+ # You can also pass a block make assertions about the contents of the
34
+ # element. Given the following HTML fragment:
35
+ #
36
+ # <turbo-stream action="replace" target="message_1">
37
+ # <template>
38
+ # <p>Hello!</p>
39
+ # <template>
40
+ # </turbo-stream>
41
+ #
42
+ # The following assertion would pass:
43
+ #
44
+ # assert_turbo_stream action: "replace", target: "message_1" do
45
+ # assert_select "template p", text: "Hello!"
46
+ # end
47
+ #
48
+ def assert_turbo_stream(action:, target: nil, targets: nil, count: 1, &block)
13
49
  selector = %(turbo-stream[action="#{action}"])
14
50
  selector << %([target="#{target.respond_to?(:to_key) ? dom_id(target) : target}"]) if target
15
51
  selector << %([targets="#{targets}"]) if targets
16
- assert_select selector, count: 1, &block
52
+ assert_select selector, count: count, &block
17
53
  end
18
54
 
55
+ # Assert that the rendered fragment of HTML does not contain a `<turbo-stream>`
56
+ # element.
57
+ #
58
+ # === Options
59
+ #
60
+ # * <tt>:action</tt> [String] matches the element's <tt>[action]</tt>
61
+ # attribute
62
+ # * <tt>:target</tt> [String, #to_key] matches the element's
63
+ # <tt>[target]</tt> attribute. If the value responds to <tt>#to_key</tt>,
64
+ # the value will be transformed by calling <tt>dom_id</tt>
65
+ # * <tt>:targets</tt> [String] matches the element's <tt>[targets]</tt>
66
+ # attribute
67
+ #
68
+ # Given the following HTML fragment:
69
+ #
70
+ # <turbo-stream action="remove" target="message_1"></turbo-stream>
71
+ #
72
+ # The following assertion would fail:
73
+ #
74
+ # assert_no_turbo_stream action: "remove", target: "message_1"
75
+ #
19
76
  def assert_no_turbo_stream(action:, target: nil, targets: nil)
20
- assert_equal Mime[:turbo_stream], response.media_type
21
77
  selector = %(turbo-stream[action="#{action}"])
22
78
  selector << %([target="#{target.respond_to?(:to_key) ? dom_id(target) : target}"]) if target
23
79
  selector << %([targets="#{targets}"]) if targets
data/lib/turbo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Turbo
2
- VERSION = "1.4.0"
2
+ VERSION = "1.5.0"
3
3
  end
data/lib/turbo-rails.rb CHANGED
@@ -3,6 +3,8 @@ require "turbo/engine"
3
3
  module Turbo
4
4
  extend ActiveSupport::Autoload
5
5
 
6
+ mattr_accessor :draw_routes, default: true
7
+
6
8
  class << self
7
9
  attr_writer :signed_stream_verifier_key
8
10