From 558af7a4272b4171ccef044f8500ffa87e3cda91 Mon Sep 17 00:00:00 2001 From: TheSmartnik Date: Mon, 27 Nov 2017 14:19:11 +0300 Subject: [PATCH] Smart broadcast_to matcher (#10) Accept objects as arguments for `broadcast_to` RSpec matcher. Add optional `from_channel` modifier. --- CHANGELOG.md | 7 +++ README.md | 13 +++++ .../matchers/have_broadcasted_matcher.feature | 26 ++++++++++ lib/rspec/rails/matchers/action_cable.rb | 49 +++++++++++++++---- .../rspec/rails/matchers/action_cable_spec.rb | 26 ++++++++++ 5 files changed, 112 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cf5212..16616e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change log +## (2017-11-27) ([@thesmartnik][]) + +- Update `have_broadcasted_to` matcher to support a record as an argument. + +See https://github.com/palkan/action-cable-testing/issues/9 + ## 0.1.2 (2017-11-14) ([@palkan][]) - Add RSpec shared contexts to switch between adapters. @@ -15,3 +21,4 @@ See https://github.com/palkan/action-cable-testing/issues/4. - Initial version. ([@palkan][]) [@palkan]: https://github.com/palkan +[@thesmartnik]: https://github.com/thesmartnik diff --git a/README.md b/README.md index 68918f4..26177d9 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,19 @@ RSpec.describe CommentsController do end ``` +Or when broacasting to an object: + +```ruby +RSpec.describe CommentsController do + describe "POST #create" do + let(:post) { create :post } + + expect { post :create, comment: { text: 'Cool!', post_id: post.id } }.to + have_broadcasted_to(post).from_channel(PostChannel).with(text: 'Cool!') + end +end +``` + You can also unit-test your channels: diff --git a/features/matchers/have_broadcasted_matcher.feature b/features/matchers/have_broadcasted_matcher.feature index e84b6e3..0f065ce 100644 --- a/features/matchers/have_broadcasted_matcher.feature +++ b/features/matchers/have_broadcasted_matcher.feature @@ -97,3 +97,29 @@ Feature: have_broadcasted matcher """ When I run `rspec spec/models/broadcaster_spec.rb` Then the examples should all pass + + Scenario: Checking broadcast to a record + Given a file named "spec/channels/chat_channel_spec.rb" with: + """ruby + require "rails_helper" + + RSpec.describe ChatChannel, :type => :channel do + it "successfully subscribes" do + user = User.new(42) + + expect { + ChatChannel.broadcast_to(user, text: 'Hi') + }.to have_broadcasted_to(user) + end + end + """ + And a file named "app/models/user.rb" with: + """ruby + class User < Struct.new(:name) + def to_global_id + name + end + end + """ + When I run `rspec spec/channels/chat_channel_spec.rb` + Then the example should pass diff --git a/lib/rspec/rails/matchers/action_cable.rb b/lib/rspec/rails/matchers/action_cable.rb index decaaa1..9a605a2 100644 --- a/lib/rspec/rails/matchers/action_cable.rb +++ b/lib/rspec/rails/matchers/action_cable.rb @@ -10,8 +10,9 @@ module ActionCable # rubocop: disable Style/ClassLength # @private class HaveBroadcastedTo < RSpec::Matchers::BuiltIn::BaseMatcher - def initialize(stream) - @stream = stream + def initialize(target, channel:) + @target = target + @channel = channel @block = Proc.new {} set_expected_number(:exactly, 1) end @@ -57,7 +58,7 @@ def thrice def failure_message "expected to broadcast #{base_message}".tap do |msg| if @unmatching_msgs.any? - msg << "\nBroadcasted messages to #{@stream}:" + msg << "\nBroadcasted messages to #{stream}:" @unmatching_msgs.each do |data| msg << "\n #{data}" end @@ -84,15 +85,29 @@ def supports_block_expectations? def matches?(proc) raise ArgumentError, "have_broadcasted_to and broadcast_to only support block expectations" unless Proc === proc - original_sent_messages_count = pubsub_adapter.broadcasts(@stream).size + original_sent_messages_count = pubsub_adapter.broadcasts(stream).size proc.call - in_block_messages = pubsub_adapter.broadcasts(@stream).drop(original_sent_messages_count) + in_block_messages = pubsub_adapter.broadcasts(stream).drop(original_sent_messages_count) check(in_block_messages) end + def from_channel(channel) + @channel = channel + self + end + private + def stream + @stream ||= if @target.is_a?(String) + @target + else + check_channel_presence + @channel.broadcasting_for([@channel.channel_name, @target]) + end + end + def check(messages) @matching_msgs, @unmatching_msgs = messages.partition do |msg| decoded = ActiveSupport::JSON.decode(msg) @@ -127,7 +142,7 @@ def set_expected_number(relativity, count) end def base_message - "#{message_expectation_modifier} #{@expected_number} messages to #{@stream}".tap do |msg| + "#{message_expectation_modifier} #{@expected_number} messages to #{stream}".tap do |msg| msg << " with #{data_description(@data)}" unless @data.nil? msg << ", but broadcast #{@matching_msgs_count}" end @@ -144,18 +159,32 @@ def data_description(data) def pubsub_adapter ::ActionCable.server.pubsub end + + def check_channel_presence + return if @channel.present? && @channel.respond_to?(:channel_name) + + error_msg = "Broadcastnig channel can't be infered. Please, specify it with `from_channel`" + raise ArgumentError, error_msg + end end # rubocop: enable Style/ClassLength end # @api public - # Passes if a message has been sent to a stream inside block. May chain at_least, at_most or exactly to specify a number of times. + # Passes if a message has been sent to a stream/object inside a block. + # May chain `at_least`, `at_most` or `exactly` to specify a number of times. + # To specify channel from which message has been broadcasted to object use `from_channel`. + # # # @example # expect { # ActionCable.server.broadcast "messages", text: 'Hi!' # }.to have_broadcasted_to("messages") # + # expect { + # SomeChannel.broadcast_to(user) + # }.to have_broadcasted_to(user).from_channel(SomeChannel) + # # # Using alias # expect { # ActionCable.server.broadcast "messages", text: 'Hi!' @@ -177,9 +206,11 @@ def pubsub_adapter # expect { # ActionCable.server.broadcast "messages", text: 'Hi!' # }.to have_broadcasted_to("messages").with(text: 'Hi!') - def have_broadcasted_to(stream = nil) + + def have_broadcasted_to(target = nil) check_action_cable_adapter - ActionCable::HaveBroadcastedTo.new(stream) + + ActionCable::HaveBroadcastedTo.new(target, channel: described_class) end alias_method :broadcast_to, :have_broadcasted_to diff --git a/spec/rspec/rails/matchers/action_cable_spec.rb b/spec/rspec/rails/matchers/action_cable_spec.rb index 1e4a60d..1a1b537 100644 --- a/spec/rspec/rails/matchers/action_cable_spec.rb +++ b/spec/rspec/rails/matchers/action_cable_spec.rb @@ -5,6 +5,9 @@ require "rspec/rails/matchers/action_cable" end +class BroadcastToChannel < ActionCable::Channel::Base +end + RSpec.describe "ActionCable matchers", :skip => !RSpec::Rails::FeatureCheck.has_action_cable? do def broadcast(stream, msg) ActionCable.server.broadcast stream, msg @@ -171,5 +174,28 @@ def broadcast(stream, msg) expect(e.message).to match(/got: "asdf"/) } end + + + context "when object is passed as first argument" do + let(:user) { User.new(42) } + + context "when channel is present" do + it "passes" do + expect { + BroadcastToChannel.broadcast_to(user, text: 'Hi') + }.to have_broadcasted_to(user).from_channel(BroadcastToChannel) + end + end + + context "when channel can't be infered" do + it "raises exception" do + expect { + expect { + BroadcastToChannel.broadcast_to(user, text: 'Hi') + }.to have_broadcasted_to(user) + }.to raise_error(ArgumentError) + end + end + end end end