turbo-rails 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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