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.
- checksums.yaml +4 -4
- data/README.md +47 -2
- data/app/assets/javascripts/turbo.js +3 -1
- data/app/assets/javascripts/turbo.min.js +1 -1
- data/app/assets/javascripts/turbo.min.js.map +1 -1
- data/app/controllers/turbo/frames/frame_request.rb +2 -0
- data/app/controllers/turbo/native/navigation.rb +16 -8
- data/app/helpers/turbo/frames_helper.rb +1 -1
- data/app/helpers/turbo/streams/action_helper.rb +14 -2
- data/app/javascript/turbo/fetch_requests.js +10 -1
- data/app/models/concerns/turbo/broadcastable.rb +14 -1
- data/config/routes.rb +1 -1
- data/lib/install/turbo_with_bun.rb +9 -0
- data/lib/tasks/turbo_tasks.rake +35 -18
- data/lib/turbo/broadcastable/test_helper.rb +172 -0
- data/lib/turbo/engine.rb +15 -1
- data/lib/turbo/test_assertions/integration_test_assertions.rb +76 -0
- data/lib/turbo/test_assertions.rb +61 -5
- data/lib/turbo/version.rb +1 -1
- data/lib/turbo-rails.rb +2 -0
- metadata +6 -3
@@ -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
|
-
#
|
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
|
-
|
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.
|
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
|
-
|
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"
|
data/lib/tasks/turbo_tasks.rake
CHANGED
@@ -1,18 +1,27 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
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
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
11
|
-
|
12
|
-
|
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:
|
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