require "test_helper"
def with_mocked_renderer
renderer = Minitest::Mock.new
Futurism::Resolver::Controller::Renderer.stub(:for, renderer) do
yield(renderer)
end
end
def with_mocked_cable_ready
cable_ready_mock = MiniTest::Mock.new
cable_ready_channel = MiniTest::Mock.new
cable_ready_channel.expect(:outer_html, nil, [Hash])
cable_ready_channel.expect(:outer_html, nil, [Hash])
cable_ready_mock.expect(:[], cable_ready_channel, ["1"])
cable_ready_mock.expect(:[], cable_ready_channel, ["1"])
cable_ready_mock.expect(:broadcast, nil)
cable_ready_mock.expect(:broadcast, nil)
cable_ready_mock.expect(:broadcast, nil)
CableReady::Broadcaster.alias_method(:orig_cable_ready, :cable_ready)
CableReady::Broadcaster.define_method(:cable_ready) do
cable_ready_mock
end
yield cable_ready_mock
CableReady::Broadcaster.undef_method(:cable_ready)
CableReady::Broadcaster.alias_method(:cable_ready, :orig_cable_ready)
end
class Futurism::ChannelTest < ActionCable::Channel::TestCase
include Futurism::Helpers
include ActionView::Helpers
include ActionView::Context
include CableReady::Broadcaster
setup do
stub_connection(env: {"SCRIPT_NAME" => "/cable"}, identifiers: [:current_user], current_user: Struct.new(:id)[1])
end
test "subscribed" do
subscribe(channel: "Futurism::Channel")
assert subscription.confirmed?
assert_has_stream "Futurism::Channel:1"
end
test "broadcasts a rendered model after receiving signed params" do
with_mocked_renderer do |mock_renderer|
post = Post.create title: "Lorem"
fragment = Nokogiri::HTML.fragment(futurize(post) {})
signed_params_array = fragment.children.map { |element| element["data-signed-params"] }
sgids = fragment.children.map { |element| element["data-sgid"] }
subscribe
mock_renderer.expect :render, "", [post]
perform :receive, {"signed_params" => signed_params_array, "sgids" => sgids}
assert_mock mock_renderer
end
end
test "broadcasts an ActiveRecord::Relation" do
with_mocked_renderer do |mock_renderer|
post1 = Post.create(title: "Lorem")
post2 = Post.create(title: "Ipsum")
fragment = Nokogiri::HTML.fragment(futurize(Post.all) {})
signed_params_array = fragment.children.map { |element| element["data-signed-params"] }
sgids = fragment.children.map { |element| element["data-sgid"] }
subscribe
mock_renderer
.expect(:render, "", [post1])
.expect :render, "", [post2]
perform :receive, {"signed_params" => signed_params_array, "sgids" => sgids}
assert_mock mock_renderer
end
end
test "broadcasts a rendered partial after receiving signed params" do
with_mocked_renderer do |mock_renderer|
post = Post.create title: "Lorem"
fragment = Nokogiri::HTML.fragment(futurize(partial: "posts/card", locals: {post: post}) {})
signed_params = fragment.children.first["data-signed-params"]
subscribe
mock_renderer
.expect(:render, "", [partial: "posts/card", locals: {post: post}])
perform :receive, {"signed_params" => [signed_params]}
assert_mock mock_renderer
end
end
test "broadcasts a rendered partial after receiving the shorthand syntax" do
with_mocked_renderer do |mock_renderer|
post = Post.create title: "Lorem"
fragment = Nokogiri::HTML.fragment(futurize("posts/card", post: post) {})
signed_params = fragment.children.first["data-signed-params"]
subscribe
mock_renderer.expect(:render, "", [partial: "posts/card", locals: {post: post}])
perform :receive, {"signed_params" => [signed_params]}
assert_mock mock_renderer
end
end
test "broadcasts a rendered partial after receiving the shorthand syntax with html options" do
with_mocked_renderer do |mock_renderer|
post = Post.create title: "Lorem"
fragment = Nokogiri::HTML.fragment(futurize("posts/card", post: post, html_options: {style: "color: green"}) {})
signed_params = fragment.children.first["data-signed-params"]
subscribe
mock_renderer.expect(:render, "", [partial: "posts/card", locals: {post: post}])
perform :receive, {"signed_params" => [signed_params]}
assert_mock mock_renderer
end
end
test "broadcasts a collection" do
with_mocked_renderer do |mock_renderer|
Post.create title: "Lorem"
Post.create title: "Ipsum"
fragment = Nokogiri::HTML.fragment(futurize(partial: "posts/card", collection: Post.all, locals: {important_local: "needed to render"}) {})
subscribe
mock_renderer
.expect(:render, "", [partial: "posts/card", locals: {post: Post.first, important_local: "needed to render", post_counter: 0}])
.expect(:render, "", [partial: "posts/card", locals: {post: Post.last, important_local: "needed to render", post_counter: 1}])
signed_params = fragment.children.first["data-signed-params"]
perform :receive, {"signed_params" => [signed_params]}
signed_params = fragment.children.last["data-signed-params"]
perform :receive, {"signed_params" => [signed_params]}
assert_mock mock_renderer
end
end
test "broadcasts a collection (with multi-word class)" do
with_mocked_renderer do |mock_renderer|
ActionItem.create description: "Do this"
ActionItem.create description: "Do that"
fragment = Nokogiri::HTML.fragment(futurize(partial: "posts/card", collection: ActionItem.all, locals: {important_local: "needed to render"}) {})
subscribe
mock_renderer
.expect(:render, "", [partial: "posts/card", locals: {action_item: ActionItem.first, important_local: "needed to render", action_item_counter: 0}])
.expect(:render, "", [partial: "posts/card", locals: {action_item: ActionItem.last, important_local: "needed to render", action_item_counter: 1}])
signed_params = fragment.children.first["data-signed-params"]
perform :receive, {"signed_params" => [signed_params]}
signed_params = fragment.children.last["data-signed-params"]
perform :receive, {"signed_params" => [signed_params]}
assert_mock mock_renderer
end
end
test "broadcasts a collection with :as" do
with_mocked_renderer do |mock_renderer|
Post.create title: "Lorem"
Post.create title: "Ipsum"
fragment = Nokogiri::HTML.fragment(futurize(partial: "posts/card", collection: Post.all, as: :post_item) {})
subscribe
mock_renderer.expect(:render, "", [partial: "posts/card", locals: {post_item: Post.first, post_item_counter: 0}])
signed_params = fragment.children.first["data-signed-params"]
perform :receive, {"signed_params" => [signed_params]}
mock_renderer.expect(:render, "", [partial: "posts/card", locals: {post_item: Post.last, post_item_counter: 1}])
signed_params = fragment.children.last["data-signed-params"]
perform :receive, {"signed_params" => [signed_params]}
assert_mock mock_renderer
end
end
test "broadcasts elements of a collection immediately" do
with_mocked_cable_ready do |cable_ready_mock|
Post.create title: "Lorem"
Post.create title: "Ipsum"
fragment = Nokogiri::HTML.fragment(futurize(partial: "posts/card", collection: Post.all, broadcast_each: true, locals: {important_local: "needed to render"}) {})
subscribe
signed_params_1 = fragment.children.first["data-signed-params"]
broadcast_each_1 = fragment.children.first["data-broadcast-each"]
signed_params_2 = fragment.children.last["data-signed-params"]
broadcast_each_2 = fragment.children.last["data-broadcast-each"]
perform :receive, {"signed_params" => [signed_params_1, signed_params_2], "broadcast_each" => [broadcast_each_1, broadcast_each_2]}
assert_mock cable_ready_mock
end
end
test "broadcasts an inline rendered text" do
fragment = Nokogiri::HTML.fragment(futurize(inline: "<%= 1 + 2 %>") {})
signed_params = fragment.children.first["data-signed-params"]
subscribe(channel: "Futurism::Channel")
assert_cable_ready_operation_on("Futurism::Channel:1", operation: "outerHtml", selector: "[data-signed-params='#{signed_params}']", html: "3") do
perform :receive, {"signed_params" => [signed_params]}
end
end
test "broadcasts a correctly formed path" do
post = Post.create title: "Lorem"
fragment = Nokogiri::HTML.fragment(futurize(partial: "posts/card", locals: {post: post}) {})
signed_params = fragment.children.first["data-signed-params"]
subscribe(channel: "Futurism::Channel")
assert_cable_ready_operation_on("Futurism::Channel:1", operation: "outerHtml",
selector: "[data-signed-params='#{signed_params}']",
html: "
\n") do
perform :receive, {"signed_params" => [signed_params]}
end
end
test "passes parsed params to controller render" do
with_mocked_renderer do |mock_renderer|
post = Post.create title: "Lorem"
fragment = Nokogiri::HTML.fragment(futurize(post) {})
signed_params_array = fragment.children.map { |element| element["data-signed-params"] }
sgids = fragment.children.map { |element| element["data-sgid"] }
urls = Array.new(fragment.children.length, "http://www.example.org/route?param1=true¶m2=1234")
subscribe
mock_renderer.expect(:render, "", [post])
perform :receive, {"signed_params" => signed_params_array, "sgids" => sgids, "urls" => urls}
assert_mock mock_renderer
end
end
test "renders error message when rendering invalid partial error" do
fragment = Nokogiri::HTML.fragment(futurize(partial: "INVALID/PARTIAL") {})
signed_params = fragment.children.first["data-signed-params"]
subscribe(channel: "Futurism::Channel")
assert_cable_ready_operation_on("Futurism::Channel:1", operation: "outerHtml",
selector: "[data-signed-params='#{signed_params}']",
html: /Missing partial INVALID\/_PARTIAL/) do
perform :receive, {"signed_params" => [signed_params]}
end
end
test "renders error message when wrong variable name" do
Post.create title: "Lorem"
fragment = Nokogiri::HTML.fragment(futurize(partial: "posts/card", collection: Post.all, as: :wrong_variable_name) {})
signed_params = fragment.children.first["data-signed-params"]
subscribe(channel: "Futurism::Channel")
assert_cable_ready_operation_on("Futurism::Channel:1", operation: "outerHtml",
selector: "[data-signed-params='#{signed_params}']",
html: /undefined local variable or method/) do
perform :receive, {"signed_params" => [signed_params]}
end
end
def assert_cable_ready_operation_on(stream, operation:, selector:, html:, &block)
data = {
"cableReady" => true,
"operations" => [{
"selector" => selector,
"html" => html,
"operation" => operation
}]
}
old_messages = broadcasts(stream)
clear_messages(stream)
assert_nothing_raised(&block)
new_messages = broadcasts(stream)
clear_messages(stream)
# Restore all sent messages
(old_messages + new_messages).each { |m| pubsub_adapter.broadcast(stream, m) }
message = new_messages.find { |msg| cable_ready_match?(ActiveSupport::JSON.decode(msg), data) }
assert message, "No messages sent with #{data} to #{stream}"
end
def cable_ready_match?(message, matcher)
return true if message == matcher
first_matching_operation = ["operations", 0]
matcher_operation = matcher.dig(*first_matching_operation)
message_operation = message.dig(*first_matching_operation)
message.dig("cableReady") == true &&
(matcher_operation.dig("selector") === message_operation.dig("selector") ||
matcher_operation.dig("selector").match(message_operation.dig("selector"))) &&
(matcher_operation.dig("html") === message_operation.dig("html") ||
matcher_operation.dig("html").match(message_operation.dig("html")))
end
end