turbo-rails 1.3.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.
- checksums.yaml +4 -4
- data/README.md +48 -3
- data/Rakefile +15 -2
- data/app/assets/javascripts/turbo.js +366 -191
- data/app/assets/javascripts/turbo.min.js +5 -5
- data/app/assets/javascripts/turbo.min.js.map +1 -1
- data/app/controllers/turbo/frames/frame_request.rb +17 -7
- data/app/controllers/turbo/native/navigation.rb +19 -9
- data/app/helpers/turbo/frames_helper.rb +1 -1
- data/app/helpers/turbo/streams/action_helper.rb +17 -5
- data/app/javascript/turbo/cable_stream_source_element.js +17 -2
- data/app/javascript/turbo/fetch_requests.js +43 -3
- data/app/models/concerns/turbo/broadcastable.rb +42 -10
- data/app/models/turbo/streams/tag_builder.rb +2 -0
- data/app/views/layouts/turbo_rails/frame.html.erb +8 -0
- 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 +7 -3
@@ -1,20 +1,30 @@
|
|
1
1
|
# Turbo frame requests are requests made from within a turbo frame with the intention of replacing the content of just
|
2
2
|
# that frame, not the whole page. They are automatically tagged as such by the Turbo Frame JavaScript, which adds a
|
3
|
-
# <tt>Turbo-Frame</tt> header to the request.
|
4
|
-
# template layout is skipped (since we're only working on an in-page frame, thus can skip the weight of the layout), and
|
5
|
-
# that the etag for the page is changed (such that a cache for a layout-less request isn't served on a normal request
|
6
|
-
# and vice versa).
|
3
|
+
# <tt>Turbo-Frame</tt> header to the request.
|
7
4
|
#
|
8
|
-
#
|
9
|
-
#
|
5
|
+
# When that header is detected by the controller, we substitute our own minimal layout in place of the
|
6
|
+
# application-supplied layout (since we're only working on an in-page frame, thus can skip the weight of the layout). We
|
7
|
+
# use a minimal layout, rather than avoid the layout entirely, so that it's still possible to render content into the
|
8
|
+
# <tt>head<tt>.
|
9
|
+
#
|
10
|
+
# Accordingly, we ensure that the etag for the page is changed, such that a cache for a minimal-layout request isn't
|
11
|
+
# served on a normal request and vice versa.
|
12
|
+
#
|
13
|
+
# This is merely a rendering optimization. Everything would still work just fine if we rendered everything including the
|
14
|
+
# full layout. Turbo Frames knows how to fish out the relevant frame regardless.
|
15
|
+
#
|
16
|
+
# The layout used is <tt>turbo_rails/frame.html.erb</tt>. If there's a need to customize this layout, an application can
|
17
|
+
# supply its own (such as <tt>app/views/layouts/turbo_rails/frame.html.erb</tt>) which will be used instead.
|
10
18
|
#
|
11
19
|
# This module is automatically included in <tt>ActionController::Base</tt>.
|
12
20
|
module Turbo::Frames::FrameRequest
|
13
21
|
extend ActiveSupport::Concern
|
14
22
|
|
15
23
|
included do
|
16
|
-
layout -> {
|
24
|
+
layout -> { "turbo_rails/frame" if turbo_frame_request? }
|
17
25
|
etag { :frame if turbo_frame_request? }
|
26
|
+
|
27
|
+
helper_method :turbo_frame_request_id
|
18
28
|
end
|
19
29
|
|
20
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
|
-
#
|
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,20 +41,19 @@ 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 = {})
|
49
|
+
native_params = options.delete(:native_params) || {}
|
50
|
+
|
36
51
|
if turbo_native_app?
|
37
|
-
redirect_to send("turbo_#{action}_historical_location_url", notice: options[:notice]
|
52
|
+
redirect_to send("turbo_#{action}_historical_location_url", notice: options[:notice], **native_params)
|
38
53
|
elsif redirect_type == :back
|
39
54
|
redirect_back fallback_location: url, **options
|
40
55
|
else
|
41
56
|
redirect_to url, options
|
42
57
|
end
|
43
58
|
end
|
44
|
-
|
45
|
-
# Turbo Native applications are identified by having the string "Turbo Native" as part of their user agent.
|
46
|
-
def turbo_native_app?
|
47
|
-
request.user_agent.to_s.match?(/Turbo Native/)
|
48
|
-
end
|
49
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,22 +11,34 @@ 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
|
|
17
29
|
if target = convert_to_turbo_stream_dom_id(target)
|
18
|
-
tag.turbo_stream(template, **attributes
|
30
|
+
tag.turbo_stream(template, **attributes, action: action, target: target)
|
19
31
|
elsif targets = convert_to_turbo_stream_dom_id(targets, include_selector: true)
|
20
|
-
tag.turbo_stream(template, **attributes
|
32
|
+
tag.turbo_stream(template, **attributes, action: action, targets: targets)
|
21
33
|
else
|
22
|
-
tag.turbo_stream(template, **attributes
|
34
|
+
tag.turbo_stream(template, **attributes, action: action)
|
23
35
|
end
|
24
36
|
end
|
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
|
-
|
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
|
@@ -5,7 +5,11 @@ import snakeize from "./snakeize"
|
|
5
5
|
class TurboCableStreamSourceElement extends HTMLElement {
|
6
6
|
async connectedCallback() {
|
7
7
|
connectStreamSource(this)
|
8
|
-
this.subscription = await subscribeTo(this.channel, {
|
8
|
+
this.subscription = await subscribeTo(this.channel, {
|
9
|
+
received: this.dispatchMessageEvent.bind(this),
|
10
|
+
connected: this.subscriptionConnected.bind(this),
|
11
|
+
disconnected: this.subscriptionDisconnected.bind(this)
|
12
|
+
})
|
9
13
|
}
|
10
14
|
|
11
15
|
disconnectedCallback() {
|
@@ -18,6 +22,14 @@ class TurboCableStreamSourceElement extends HTMLElement {
|
|
18
22
|
return this.dispatchEvent(event)
|
19
23
|
}
|
20
24
|
|
25
|
+
subscriptionConnected() {
|
26
|
+
this.setAttribute("connected", "")
|
27
|
+
}
|
28
|
+
|
29
|
+
subscriptionDisconnected() {
|
30
|
+
this.removeAttribute("connected")
|
31
|
+
}
|
32
|
+
|
21
33
|
get channel() {
|
22
34
|
const channel = this.getAttribute("channel")
|
23
35
|
const signed_stream_name = this.getAttribute("signed-stream-name")
|
@@ -25,4 +37,7 @@ class TurboCableStreamSourceElement extends HTMLElement {
|
|
25
37
|
}
|
26
38
|
}
|
27
39
|
|
28
|
-
|
40
|
+
|
41
|
+
if (customElements.get("turbo-cable-stream-source") === undefined) {
|
42
|
+
customElements.define("turbo-cable-stream-source", TurboCableStreamSourceElement)
|
43
|
+
}
|
@@ -3,13 +3,14 @@ export function encodeMethodIntoRequestBody(event) {
|
|
3
3
|
const { target: form, detail: { fetchOptions } } = event
|
4
4
|
|
5
5
|
form.addEventListener("turbo:submit-start", ({ detail: { formSubmission: { submitter } } }) => {
|
6
|
-
const
|
6
|
+
const body = isBodyInit(fetchOptions.body) ? fetchOptions.body : new URLSearchParams()
|
7
|
+
const method = determineFetchMethod(submitter, body, form)
|
7
8
|
|
8
9
|
if (!/get/i.test(method)) {
|
9
10
|
if (/post/i.test(method)) {
|
10
|
-
|
11
|
+
body.delete("_method")
|
11
12
|
} else {
|
12
|
-
|
13
|
+
body.set("_method", method)
|
13
14
|
}
|
14
15
|
|
15
16
|
fetchOptions.method = "post"
|
@@ -17,3 +18,42 @@ export function encodeMethodIntoRequestBody(event) {
|
|
17
18
|
}, { once: true })
|
18
19
|
}
|
19
20
|
}
|
21
|
+
|
22
|
+
function determineFetchMethod(submitter, body, form) {
|
23
|
+
const formMethod = determineFormMethod(submitter)
|
24
|
+
const overrideMethod = body.get("_method")
|
25
|
+
const method = form.getAttribute("method") || "get"
|
26
|
+
|
27
|
+
if (typeof formMethod == "string") {
|
28
|
+
return formMethod
|
29
|
+
} else if (typeof overrideMethod == "string") {
|
30
|
+
return overrideMethod
|
31
|
+
} else {
|
32
|
+
return method
|
33
|
+
}
|
34
|
+
}
|
35
|
+
|
36
|
+
function determineFormMethod(submitter) {
|
37
|
+
if (submitter instanceof HTMLButtonElement || submitter instanceof HTMLInputElement) {
|
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")) {
|
48
|
+
return submitter.formMethod
|
49
|
+
} else {
|
50
|
+
return null
|
51
|
+
}
|
52
|
+
} else {
|
53
|
+
return null
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
function isBodyInit(body) {
|
58
|
+
return body instanceof FormData || body instanceof URLSearchParams
|
59
|
+
}
|
@@ -27,8 +27,8 @@
|
|
27
27
|
# (which is derived by default from the plural model name of the model, but can be overwritten).
|
28
28
|
#
|
29
29
|
# You can also choose to render html instead of a partial inside of a broadcast
|
30
|
-
# you do this by passing the html
|
31
|
-
#
|
30
|
+
# you do this by passing the `html:` option to any broadcast method that accepts the **rendering argument. Example:
|
31
|
+
#
|
32
32
|
# class Message < ApplicationRecord
|
33
33
|
# belongs_to :user
|
34
34
|
#
|
@@ -39,7 +39,34 @@
|
|
39
39
|
# broadcast_update_to(user, :messages, target: "message-count", html: "<p> #{user.messages.count} </p>")
|
40
40
|
# end
|
41
41
|
# end
|
42
|
-
#
|
42
|
+
#
|
43
|
+
# If you want to render a template instead of a partial, e.g. ('messages/index' or 'messages/show'), you can use the `template:` option.
|
44
|
+
# Again, only to any broadcast method that accepts the `**rendering` argument. Example:
|
45
|
+
#
|
46
|
+
# class Message < ApplicationRecord
|
47
|
+
# belongs_to :user
|
48
|
+
#
|
49
|
+
# after_create_commit :update_message
|
50
|
+
#
|
51
|
+
# private
|
52
|
+
# def update_message
|
53
|
+
# broadcast_replace_to(user, :message, target: "message", template: "messages/show", locals: { message: self })
|
54
|
+
# end
|
55
|
+
# end
|
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
|
+
#
|
43
70
|
# There are four basic actions you can broadcast: <tt>remove</tt>, <tt>replace</tt>, <tt>append</tt>, and
|
44
71
|
# <tt>prepend</tt>. As a rule, you should use the <tt>_later</tt> versions of everything except for remove when broadcasting
|
45
72
|
# within a real-time path, like a controller or model, since all those updates require a rendering step, which can slow down
|
@@ -72,16 +99,16 @@ module Turbo::Broadcastable
|
|
72
99
|
# broadcasts_to ->(message) { [ message.board, :messages ] }, partial: "messages/custom_message"
|
73
100
|
# end
|
74
101
|
def broadcasts_to(stream, inserts_by: :append, target: broadcast_target_default, **rendering)
|
75
|
-
after_create_commit -> { broadcast_action_later_to
|
76
|
-
after_update_commit -> { broadcast_replace_later_to
|
77
|
-
after_destroy_commit -> { broadcast_remove_to
|
102
|
+
after_create_commit -> { broadcast_action_later_to(stream.try(:call, self) || send(stream), action: inserts_by, target: target.try(:call, self) || target, **rendering) }
|
103
|
+
after_update_commit -> { broadcast_replace_later_to(stream.try(:call, self) || send(stream), **rendering) }
|
104
|
+
after_destroy_commit -> { broadcast_remove_to(stream.try(:call, self) || send(stream)) }
|
78
105
|
end
|
79
106
|
|
80
107
|
# Same as <tt>#broadcasts_to</tt>, but the designated stream for updates and destroys is automatically set to
|
81
108
|
# the current model, for creates - to the model plural name, which can be overriden by passing <tt>stream</tt>.
|
82
109
|
def broadcasts(stream = model_name.plural, inserts_by: :append, target: broadcast_target_default, **rendering)
|
83
|
-
after_create_commit -> { broadcast_action_later_to
|
84
|
-
after_update_commit -> { broadcast_replace_later
|
110
|
+
after_create_commit -> { broadcast_action_later_to(stream, action: inserts_by, target: target.try(:call, self) || target, **rendering) }
|
111
|
+
after_update_commit -> { broadcast_replace_later(**rendering) }
|
85
112
|
after_destroy_commit -> { broadcast_remove }
|
86
113
|
end
|
87
114
|
|
@@ -335,8 +362,13 @@ module Turbo::Broadcastable
|
|
335
362
|
# Add the current instance into the locals with the element name (which is the un-namespaced name)
|
336
363
|
# as the key. This parallels how the ActionView::ObjectRenderer would create a local variable.
|
337
364
|
o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.element.to_sym => self)
|
338
|
-
|
339
|
-
|
365
|
+
|
366
|
+
if o[:html] || o[:partial]
|
367
|
+
return o
|
368
|
+
elsif o[:template] || o[:renderable]
|
369
|
+
o[:layout] = false
|
370
|
+
else
|
371
|
+
# if none of these options are passed in, it will set a partial from #to_partial_path
|
340
372
|
o[:partial] ||= to_partial_path
|
341
373
|
end
|
342
374
|
end
|
@@ -227,6 +227,8 @@ class Turbo::Streams::TagBuilder
|
|
227
227
|
private
|
228
228
|
def render_template(target, content = nil, allow_inferred_rendering: true, **rendering, &block)
|
229
229
|
case
|
230
|
+
when content.respond_to?(:render_in)
|
231
|
+
content.render_in(@view_context, &block)
|
230
232
|
when content
|
231
233
|
allow_inferred_rendering ? (render_record(content) || content) : content
|
232
234
|
when block_given?
|
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
|