aboutsummaryrefslogtreecommitdiffstats
path: root/actioncable/lib
diff options
context:
space:
mode:
authorWillian Gustavo Veiga <beberveiga@gmail.com>2018-10-02 12:55:36 -0300
committerWillian Gustavo Veiga <beberveiga@gmail.com>2018-10-02 12:57:37 -0300
commit2d4df1349efdf8dd2c8cc4503fd5a871b0066500 (patch)
tree9f71a297cf1bb30f1d59039997a60f97357d0782 /actioncable/lib
parent00c50c2b5966fa1d719c8a58564811c672a0e8c6 (diff)
parentcf608ee34dd833b0357ef4eefa692db33242d2aa (diff)
downloadrails-2d4df1349efdf8dd2c8cc4503fd5a871b0066500.tar.gz
rails-2d4df1349efdf8dd2c8cc4503fd5a871b0066500.tar.bz2
rails-2d4df1349efdf8dd2c8cc4503fd5a871b0066500.zip
Merge branch 'master' into feature/reselect-method
Diffstat (limited to 'actioncable/lib')
-rw-r--r--actioncable/lib/action_cable.rb14
-rw-r--r--actioncable/lib/action_cable/channel.rb1
-rw-r--r--actioncable/lib/action_cable/channel/base.rb2
-rw-r--r--actioncable/lib/action_cable/channel/test_case.rb271
-rw-r--r--actioncable/lib/action_cable/subscription_adapter.rb1
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/redis.rb3
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/test.rb40
-rw-r--r--actioncable/lib/action_cable/test_case.rb11
-rw-r--r--actioncable/lib/action_cable/test_helper.rb133
-rw-r--r--actioncable/lib/rails/generators/channel/USAGE8
-rw-r--r--actioncable/lib/rails/generators/channel/channel_generator.rb5
-rw-r--r--actioncable/lib/rails/generators/channel/templates/assets/channel.coffee.tt14
-rw-r--r--actioncable/lib/rails/generators/channel/templates/javascript/channel.js.tt (renamed from actioncable/lib/rails/generators/channel/templates/assets/channel.js.tt)4
-rw-r--r--actioncable/lib/rails/generators/channel/templates/javascript/consumer.js.tt (renamed from actioncable/lib/rails/generators/channel/templates/assets/cable.js.tt)11
-rw-r--r--actioncable/lib/rails/generators/channel/templates/javascript/index.js.tt5
15 files changed, 484 insertions, 39 deletions
diff --git a/actioncable/lib/action_cable.rb b/actioncable/lib/action_cable.rb
index e7456e3c1b..d261d4112e 100644
--- a/actioncable/lib/action_cable.rb
+++ b/actioncable/lib/action_cable.rb
@@ -32,13 +32,13 @@ module ActionCable
INTERNAL = {
message_types: {
- welcome: "welcome".freeze,
- ping: "ping".freeze,
- confirmation: "confirm_subscription".freeze,
- rejection: "reject_subscription".freeze
+ welcome: "welcome",
+ ping: "ping",
+ confirmation: "confirm_subscription",
+ rejection: "reject_subscription"
},
- default_mount_path: "/cable".freeze,
- protocols: ["actioncable-v1-json".freeze, "actioncable-unsupported".freeze].freeze
+ default_mount_path: "/cable",
+ protocols: ["actioncable-v1-json", "actioncable-unsupported"].freeze
}
# Singleton instance of the server
@@ -51,4 +51,6 @@ module ActionCable
autoload :Channel
autoload :RemoteConnections
autoload :SubscriptionAdapter
+ autoload :TestHelper
+ autoload :TestCase
end
diff --git a/actioncable/lib/action_cable/channel.rb b/actioncable/lib/action_cable/channel.rb
index d2f6fbbbc7..d5118b9dc9 100644
--- a/actioncable/lib/action_cable/channel.rb
+++ b/actioncable/lib/action_cable/channel.rb
@@ -11,6 +11,7 @@ module ActionCable
autoload :Naming
autoload :PeriodicTimers
autoload :Streams
+ autoload :TestCase
end
end
end
diff --git a/actioncable/lib/action_cable/channel/base.rb b/actioncable/lib/action_cable/channel/base.rb
index c5ad749bfe..70c93ec0f3 100644
--- a/actioncable/lib/action_cable/channel/base.rb
+++ b/actioncable/lib/action_cable/channel/base.rb
@@ -270,7 +270,7 @@ module ActionCable
end
def action_signature(action, data)
- "#{self.class.name}##{action}".dup.tap do |signature|
+ (+"#{self.class.name}##{action}").tap do |signature|
if (arguments = data.except("action")).any?
signature << "(#{arguments.inspect})"
end
diff --git a/actioncable/lib/action_cable/channel/test_case.rb b/actioncable/lib/action_cable/channel/test_case.rb
new file mode 100644
index 0000000000..dd2762ccd0
--- /dev/null
+++ b/actioncable/lib/action_cable/channel/test_case.rb
@@ -0,0 +1,271 @@
+# frozen_string_literal: true
+
+require "active_support"
+require "active_support/test_case"
+require "active_support/core_ext/hash/indifferent_access"
+require "json"
+
+module ActionCable
+ module Channel
+ class NonInferrableChannelError < ::StandardError
+ def initialize(name)
+ super "Unable to determine the channel to test from #{name}. " +
+ "You'll need to specify it using `tests YourChannel` in your " +
+ "test case definition."
+ end
+ end
+
+ # Stub `stream_from` to track streams for the channel.
+ # Add public aliases for `subscription_confirmation_sent?` and
+ # `subscription_rejected?`.
+ module ChannelStub
+ def confirmed?
+ subscription_confirmation_sent?
+ end
+
+ def rejected?
+ subscription_rejected?
+ end
+
+ def stream_from(broadcasting, *)
+ streams << broadcasting
+ end
+
+ def stop_all_streams
+ @_streams = []
+ end
+
+ def streams
+ @_streams ||= []
+ end
+
+ # Make periodic timers no-op
+ def start_periodic_timers; end
+ alias stop_periodic_timers start_periodic_timers
+ end
+
+ class ConnectionStub
+ attr_reader :transmissions, :identifiers, :subscriptions, :logger
+
+ def initialize(identifiers = {})
+ @transmissions = []
+
+ identifiers.each do |identifier, val|
+ define_singleton_method(identifier) { val }
+ end
+
+ @subscriptions = ActionCable::Connection::Subscriptions.new(self)
+ @identifiers = identifiers.keys
+ @logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new)
+ end
+
+ def transmit(cable_message)
+ transmissions << cable_message.with_indifferent_access
+ end
+ end
+
+ # Superclass for Action Cable channel functional tests.
+ #
+ # == Basic example
+ #
+ # Functional tests are written as follows:
+ # 1. First, one uses the +subscribe+ method to simulate subscription creation.
+ # 2. Then, one asserts whether the current state is as expected. "State" can be anything:
+ # transmitted messages, subscribed streams, etc.
+ #
+ # For example:
+ #
+ # class ChatChannelTest < ActionCable::Channel::TestCase
+ # def test_subscribed_with_room_number
+ # # Simulate a subscription creation
+ # subscribe room_number: 1
+ #
+ # # Asserts that the subscription was successfully created
+ # assert subscription.confirmed?
+ #
+ # # Asserts that the channel subscribes connection to a stream
+ # assert_equal "chat_1", streams.last
+ # end
+ #
+ # def test_does_not_subscribe_without_room_number
+ # subscribe
+ #
+ # # Asserts that the subscription was rejected
+ # assert subscription.rejected?
+ # end
+ # end
+ #
+ # You can also perform actions:
+ # def test_perform_speak
+ # subscribe room_number: 1
+ #
+ # perform :speak, message: "Hello, Rails!"
+ #
+ # assert_equal "Hello, Rails!", transmissions.last["text"]
+ # end
+ #
+ # == Special methods
+ #
+ # ActionCable::Channel::TestCase will also automatically provide the following instance
+ # methods for use in the tests:
+ #
+ # <b>connection</b>::
+ # An ActionCable::Channel::ConnectionStub, representing the current HTTP connection.
+ # <b>subscription</b>::
+ # An instance of the current channel, created when you call `subscribe`.
+ # <b>transmissions</b>::
+ # A list of all messages that have been transmitted into the channel.
+ # <b>streams</b>::
+ # A list of all created streams subscriptions (as identifiers) for the subscription.
+ #
+ #
+ # == Channel is automatically inferred
+ #
+ # ActionCable::Channel::TestCase will automatically infer the channel under test
+ # from the test class name. If the channel cannot be inferred from the test
+ # class name, you can explicitly set it with +tests+.
+ #
+ # class SpecialEdgeCaseChannelTest < ActionCable::Channel::TestCase
+ # tests SpecialChannel
+ # end
+ #
+ # == Specifying connection identifiers
+ #
+ # You need to set up your connection manually to provide values for the identifiers.
+ # To do this just use:
+ #
+ # stub_connection(user: users[:john])
+ #
+ # == Testing broadcasting
+ #
+ # ActionCable::Channel::TestCase enhances ActionCable::TestHelper assertions (e.g.
+ # +assert_broadcasts+) to handle broadcasting to models:
+ #
+ #
+ # # in your channel
+ # def speak(data)
+ # broadcast_to room, text: data["message"]
+ # end
+ #
+ # def test_speak
+ # subscribe room_id: rooms[:chat].id
+ #
+ # assert_broadcasts_on(rooms[:chat], text: "Hello, Rails!") do
+ # perform :speak, message: "Hello, Rails!"
+ # end
+ # end
+ class TestCase < ActiveSupport::TestCase
+ module Behavior
+ extend ActiveSupport::Concern
+
+ include ActiveSupport::Testing::ConstantLookup
+ include ActionCable::TestHelper
+
+ CHANNEL_IDENTIFIER = "test_stub"
+
+ included do
+ class_attribute :_channel_class
+
+ attr_reader :connection, :subscription
+ delegate :streams, to: :subscription
+
+ ActiveSupport.run_load_hooks(:action_cable_channel_test_case, self)
+ end
+
+ module ClassMethods
+ def tests(channel)
+ case channel
+ when String, Symbol
+ self._channel_class = channel.to_s.camelize.constantize
+ when Module
+ self._channel_class = channel
+ else
+ raise NonInferrableChannelError.new(channel)
+ end
+ end
+
+ def channel_class
+ if channel = self._channel_class
+ channel
+ else
+ tests determine_default_channel(name)
+ end
+ end
+
+ def determine_default_channel(name)
+ channel = determine_constant_from_test_name(name) do |constant|
+ Class === constant && constant < ActionCable::Channel::Base
+ end
+ raise NonInferrableChannelError.new(name) if channel.nil?
+ channel
+ end
+ end
+
+ # Setup test connection with the specified identifiers:
+ #
+ # class ApplicationCable < ActionCable::Connection::Base
+ # identified_by :user, :token
+ # end
+ #
+ # stub_connection(user: users[:john], token: 'my-secret-token')
+ def stub_connection(identifiers = {})
+ @connection = ConnectionStub.new(identifiers)
+ end
+
+ # Subscribe to the channel under test. Optionally pass subscription parameters as a Hash.
+ def subscribe(params = {})
+ @connection ||= stub_connection
+ @subscription = self.class.channel_class.new(connection, CHANNEL_IDENTIFIER, params.with_indifferent_access)
+ @subscription.singleton_class.include(ChannelStub)
+ @subscription.subscribe_to_channel
+ @subscription
+ end
+
+ # Unsubscribe the subscription under test.
+ def unsubscribe
+ check_subscribed!
+ subscription.unsubscribe_from_channel
+ end
+
+ # Perform action on a channel.
+ #
+ # NOTE: Must be subscribed.
+ def perform(action, data = {})
+ check_subscribed!
+ subscription.perform_action(data.stringify_keys.merge("action" => action.to_s))
+ end
+
+ # Returns messages transmitted into channel
+ def transmissions
+ # Return only directly sent message (via #transmit)
+ connection.transmissions.map { |data| data["message"] }.compact
+ end
+
+ # Enhance TestHelper assertions to handle non-String
+ # broadcastings
+ def assert_broadcasts(stream_or_object, *args)
+ super(broadcasting_for(stream_or_object), *args)
+ end
+
+ def assert_broadcast_on(stream_or_object, *args)
+ super(broadcasting_for(stream_or_object), *args)
+ end
+
+ private
+ def check_subscribed!
+ raise "Must be subscribed!" if subscription.nil? || subscription.rejected?
+ end
+
+ def broadcasting_for(stream_or_object)
+ return stream_or_object if stream_or_object.is_a?(String)
+
+ self.class.channel_class.broadcasting_for(
+ [self.class.channel_class.channel_name, stream_or_object]
+ )
+ end
+ end
+
+ include Behavior
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/subscription_adapter.rb b/actioncable/lib/action_cable/subscription_adapter.rb
index bcece8d33b..6a9d5c2080 100644
--- a/actioncable/lib/action_cable/subscription_adapter.rb
+++ b/actioncable/lib/action_cable/subscription_adapter.rb
@@ -5,6 +5,7 @@ module ActionCable
extend ActiveSupport::Autoload
autoload :Base
+ autoload :Test
autoload :SubscriberMap
autoload :ChannelPrefix
end
diff --git a/actioncable/lib/action_cable/subscription_adapter/redis.rb b/actioncable/lib/action_cable/subscription_adapter/redis.rb
index c28951608f..ad8fa52760 100644
--- a/actioncable/lib/action_cable/subscription_adapter/redis.rb
+++ b/actioncable/lib/action_cable/subscription_adapter/redis.rb
@@ -13,7 +13,8 @@ module ActionCable
# Overwrite this factory method for Redis connections if you want to use a different Redis library than the redis gem.
# This is needed, for example, when using Makara proxies for distributed Redis.
cattr_accessor :redis_connector, default: ->(config) do
- ::Redis.new(config.slice(:url, :host, :port, :db, :password))
+ config[:id] ||= "ActionCable-PID-#{$$}"
+ ::Redis.new(config.slice(:url, :host, :port, :db, :password, :id))
end
def initialize(*)
diff --git a/actioncable/lib/action_cable/subscription_adapter/test.rb b/actioncable/lib/action_cable/subscription_adapter/test.rb
new file mode 100644
index 0000000000..ce604cc88e
--- /dev/null
+++ b/actioncable/lib/action_cable/subscription_adapter/test.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require_relative "async"
+
+module ActionCable
+ module SubscriptionAdapter
+ # == Test adapter for Action Cable
+ #
+ # The test adapter should be used only in testing. Along with
+ # <tt>ActionCable::TestHelper</tt> it makes a great tool to test your Rails application.
+ #
+ # To use the test adapter set +adapter+ value to +test+ in your +config/cable.yml+ file.
+ #
+ # NOTE: Test adapter extends the <tt>ActionCable::SubscriptionsAdapter::Async</tt> adapter,
+ # so it could be used in system tests too.
+ class Test < Async
+ def broadcast(channel, payload)
+ broadcasts(channel) << payload
+ super
+ end
+
+ def broadcasts(channel)
+ channels_data[channel] ||= []
+ end
+
+ def clear_messages(channel)
+ channels_data[channel] = []
+ end
+
+ def clear
+ @channels_data = nil
+ end
+
+ private
+ def channels_data
+ @channels_data ||= {}
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/test_case.rb b/actioncable/lib/action_cable/test_case.rb
new file mode 100644
index 0000000000..d153259bf6
--- /dev/null
+++ b/actioncable/lib/action_cable/test_case.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require "active_support/test_case"
+
+module ActionCable
+ class TestCase < ActiveSupport::TestCase
+ include ActionCable::TestHelper
+
+ ActiveSupport.run_load_hooks(:action_cable_test_case, self)
+ end
+end
diff --git a/actioncable/lib/action_cable/test_helper.rb b/actioncable/lib/action_cable/test_helper.rb
new file mode 100644
index 0000000000..7bc877663c
--- /dev/null
+++ b/actioncable/lib/action_cable/test_helper.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+module ActionCable
+ # Provides helper methods for testing Action Cable broadcasting
+ module TestHelper
+ def before_setup # :nodoc:
+ server = ActionCable.server
+ test_adapter = ActionCable::SubscriptionAdapter::Test.new(server)
+
+ @old_pubsub_adapter = server.pubsub
+
+ server.instance_variable_set(:@pubsub, test_adapter)
+ super
+ end
+
+ def after_teardown # :nodoc:
+ super
+ ActionCable.server.instance_variable_set(:@pubsub, @old_pubsub_adapter)
+ end
+
+ # Asserts that the number of broadcasted messages to the stream matches the given number.
+ #
+ # def test_broadcasts
+ # assert_broadcasts 'messages', 0
+ # ActionCable.server.broadcast 'messages', { text: 'hello' }
+ # assert_broadcasts 'messages', 1
+ # ActionCable.server.broadcast 'messages', { text: 'world' }
+ # assert_broadcasts 'messages', 2
+ # end
+ #
+ # If a block is passed, that block should cause the specified number of
+ # messages to be broadcasted.
+ #
+ # def test_broadcasts_again
+ # assert_broadcasts('messages', 1) do
+ # ActionCable.server.broadcast 'messages', { text: 'hello' }
+ # end
+ #
+ # assert_broadcasts('messages', 2) do
+ # ActionCable.server.broadcast 'messages', { text: 'hi' }
+ # ActionCable.server.broadcast 'messages', { text: 'how are you?' }
+ # end
+ # end
+ #
+ def assert_broadcasts(stream, number)
+ if block_given?
+ original_count = broadcasts_size(stream)
+ yield
+ new_count = broadcasts_size(stream)
+ actual_count = new_count - original_count
+ else
+ actual_count = broadcasts_size(stream)
+ end
+
+ assert_equal number, actual_count, "#{number} broadcasts to #{stream} expected, but #{actual_count} were sent"
+ end
+
+ # Asserts that no messages have been sent to the stream.
+ #
+ # def test_no_broadcasts
+ # assert_no_broadcasts 'messages'
+ # ActionCable.server.broadcast 'messages', { text: 'hi' }
+ # assert_broadcasts 'messages', 1
+ # end
+ #
+ # If a block is passed, that block should not cause any message to be sent.
+ #
+ # def test_broadcasts_again
+ # assert_no_broadcasts 'messages' do
+ # # No job messages should be sent from this block
+ # end
+ # end
+ #
+ # Note: This assertion is simply a shortcut for:
+ #
+ # assert_broadcasts 'messages', 0, &block
+ #
+ def assert_no_broadcasts(stream, &block)
+ assert_broadcasts stream, 0, &block
+ end
+
+ # Asserts that the specified message has been sent to the stream.
+ #
+ # def test_assert_transmited_message
+ # ActionCable.server.broadcast 'messages', text: 'hello'
+ # assert_broadcast_on('messages', text: 'hello')
+ # end
+ #
+ # If a block is passed, that block should cause a message with the specified data to be sent.
+ #
+ # def test_assert_broadcast_on_again
+ # assert_broadcast_on('messages', text: 'hello') do
+ # ActionCable.server.broadcast 'messages', text: 'hello'
+ # end
+ # end
+ #
+ def assert_broadcast_on(stream, data)
+ # Encode to JSON and back–we want to use this value to compare
+ # with decoded JSON.
+ # Comparing JSON strings doesn't work due to the order if the keys.
+ serialized_msg =
+ ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(data))
+
+ new_messages = broadcasts(stream)
+ if block_given?
+ old_messages = new_messages
+ clear_messages(stream)
+
+ yield
+ new_messages = broadcasts(stream)
+ clear_messages(stream)
+
+ # Restore all sent messages
+ (old_messages + new_messages).each { |m| pubsub_adapter.broadcast(stream, m) }
+ end
+
+ message = new_messages.find { |msg| ActiveSupport::JSON.decode(msg) == serialized_msg }
+
+ assert message, "No messages sent with #{data} to #{stream}"
+ end
+
+ def pubsub_adapter # :nodoc:
+ ActionCable.server.pubsub
+ end
+
+ delegate :broadcasts, :clear_messages, to: :pubsub_adapter
+
+ private
+ def broadcasts_size(channel)
+ broadcasts(channel).size
+ end
+ end
+end
diff --git a/actioncable/lib/rails/generators/channel/USAGE b/actioncable/lib/rails/generators/channel/USAGE
index dd109fda80..ea9662436c 100644
--- a/actioncable/lib/rails/generators/channel/USAGE
+++ b/actioncable/lib/rails/generators/channel/USAGE
@@ -1,14 +1,12 @@
Description:
============
- Stubs out a new cable channel for the server (in Ruby) and client (in CoffeeScript).
+ Stubs out a new cable channel for the server (in Ruby) and client (in JavaScript).
Pass the channel name, either CamelCased or under_scored, and an optional list of channel actions as arguments.
- Note: Turn on the cable connection in app/assets/javascripts/cable.js after generating any channels.
-
Example:
========
rails generate channel Chat speak
- creates a Chat channel class and CoffeeScript asset:
+ creates a Chat channel class and JavaScript asset:
Channel: app/channels/chat_channel.rb
- Assets: app/assets/javascripts/channels/chat.coffee
+ Assets: app/javascript/channels/chat_channel.js
diff --git a/actioncable/lib/rails/generators/channel/channel_generator.rb b/actioncable/lib/rails/generators/channel/channel_generator.rb
index 427eef1f55..ef51981e89 100644
--- a/actioncable/lib/rails/generators/channel/channel_generator.rb
+++ b/actioncable/lib/rails/generators/channel/channel_generator.rb
@@ -16,10 +16,11 @@ module Rails
if options[:assets]
if behavior == :invoke
- template "assets/cable.js", "app/assets/javascripts/cable.js"
+ template "javascript/index.js", "app/javascript/channels/index.js"
+ template "javascript/consumer.js", "app/javascript/channels/consumer.js"
end
- js_template "assets/channel", File.join("app/assets/javascripts/channels", class_path, "#{file_name}")
+ js_template "javascript/channel", File.join("app/javascript/channels", class_path, "#{file_name}_channel")
end
generate_application_cable_files
diff --git a/actioncable/lib/rails/generators/channel/templates/assets/channel.coffee.tt b/actioncable/lib/rails/generators/channel/templates/assets/channel.coffee.tt
deleted file mode 100644
index 5467811aba..0000000000
--- a/actioncable/lib/rails/generators/channel/templates/assets/channel.coffee.tt
+++ /dev/null
@@ -1,14 +0,0 @@
-App.<%= class_name.underscore %> = App.cable.subscriptions.create "<%= class_name %>Channel",
- connected: ->
- # Called when the subscription is ready for use on the server
-
- disconnected: ->
- # Called when the subscription has been terminated by the server
-
- received: (data) ->
- # Called when there's incoming data on the websocket for this channel
-<% actions.each do |action| -%>
-
- <%= action %>: ->
- @perform '<%= action %>'
-<% end -%>
diff --git a/actioncable/lib/rails/generators/channel/templates/assets/channel.js.tt b/actioncable/lib/rails/generators/channel/templates/javascript/channel.js.tt
index ab0e68b11a..33baaa5a22 100644
--- a/actioncable/lib/rails/generators/channel/templates/assets/channel.js.tt
+++ b/actioncable/lib/rails/generators/channel/templates/javascript/channel.js.tt
@@ -1,4 +1,6 @@
-App.<%= class_name.underscore %> = App.cable.subscriptions.create("<%= class_name %>Channel", {
+import consumer from "./consumer"
+
+consumer.subscriptions.create("<%= class_name %>Channel", {
connected: function() {
// Called when the subscription is ready for use on the server
},
diff --git a/actioncable/lib/rails/generators/channel/templates/assets/cable.js.tt b/actioncable/lib/rails/generators/channel/templates/javascript/consumer.js.tt
index 739aa5f022..76ca3d0f2f 100644
--- a/actioncable/lib/rails/generators/channel/templates/assets/cable.js.tt
+++ b/actioncable/lib/rails/generators/channel/templates/javascript/consumer.js.tt
@@ -1,13 +1,6 @@
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
-//
-//= require action_cable
-//= require_self
-//= require_tree ./channels
-(function() {
- this.App || (this.App = {});
+import ActionCable from "actioncable"
- App.cable = ActionCable.createConsumer();
-
-}).call(this);
+export default ActionCable.createConsumer()
diff --git a/actioncable/lib/rails/generators/channel/templates/javascript/index.js.tt b/actioncable/lib/rails/generators/channel/templates/javascript/index.js.tt
new file mode 100644
index 0000000000..5da1ce2dce
--- /dev/null
+++ b/actioncable/lib/rails/generators/channel/templates/javascript/index.js.tt
@@ -0,0 +1,5 @@
+// Load all the channels within this directory and all subdirectories.
+// Channel files must be named *_channel.js.
+
+const channels = require.context('.', true, /\_channel\.js$/)
+channels.keys().forEach(channels)