stimulus_reflex 3.5.0.pre2 → 3.5.0.pre3

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of stimulus_reflex might be problematic. Click here for more details.

Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -11
  3. data/Gemfile.lock +71 -66
  4. data/README.md +4 -3
  5. data/app/channels/stimulus_reflex/channel.rb +11 -11
  6. data/lib/generators/stimulus_reflex/stimulus_reflex_generator.rb +5 -4
  7. data/lib/generators/stimulus_reflex/templates/config/initializers/stimulus_reflex.rb +5 -0
  8. data/lib/stimulus_reflex/broadcasters/broadcaster.rb +18 -17
  9. data/lib/stimulus_reflex/broadcasters/nothing_broadcaster.rb +6 -1
  10. data/lib/stimulus_reflex/broadcasters/page_broadcaster.rb +1 -3
  11. data/lib/stimulus_reflex/broadcasters/selector_broadcaster.rb +2 -6
  12. data/lib/stimulus_reflex/configuration.rb +2 -1
  13. data/lib/stimulus_reflex/dataset.rb +34 -0
  14. data/lib/stimulus_reflex/element.rb +12 -46
  15. data/lib/stimulus_reflex/reflex.rb +7 -2
  16. data/lib/stimulus_reflex/utils/attribute_builder.rb +17 -0
  17. data/lib/stimulus_reflex/utils/logger.rb +6 -2
  18. data/lib/stimulus_reflex/version.rb +1 -1
  19. data/lib/tasks/stimulus_reflex/install.rake +1 -1
  20. data/test/broadcasters/broadcaster_test.rb +0 -1
  21. data/test/broadcasters/broadcaster_test_case.rb +24 -0
  22. data/test/broadcasters/nothing_broadcaster_test.rb +14 -22
  23. data/test/broadcasters/page_broadcaster_test.rb +30 -32
  24. data/test/broadcasters/selector_broadcaster_test.rb +82 -88
  25. data/test/element_test.rb +73 -0
  26. data/test/generators/stimulus_reflex_generator_test.rb +8 -0
  27. data/test/reflex_test.rb +11 -0
  28. data/test/test_helper.rb +3 -1
  29. data/test/tmp/app/reflexes/demo_reflex.rb +0 -1
  30. metadata +36 -20
  31. data/package.json +0 -58
  32. data/stimulus_reflex.gemspec +0 -42
  33. data/yarn.lock +0 -4680
@@ -17,9 +17,7 @@ module StimulusReflex
17
17
  payload: payload,
18
18
  children_only: true,
19
19
  permanent_attribute_name: permanent_attribute_name,
20
- stimulus_reflex: data.merge({
21
- morph: to_sym
22
- })
20
+ stimulus_reflex: data.merge(morph: to_sym)
23
21
  )
24
22
  else
25
23
  operations << [update.selector, :inner_html]
@@ -27,9 +25,7 @@ module StimulusReflex
27
25
  selector: update.selector,
28
26
  html: fragment.to_html,
29
27
  payload: payload,
30
- stimulus_reflex: data.merge({
31
- morph: to_sym
32
- })
28
+ stimulus_reflex: data.merge(morph: to_sym)
33
29
  )
34
30
  end
35
31
  end
@@ -14,7 +14,7 @@ module StimulusReflex
14
14
  end
15
15
 
16
16
  class Configuration
17
- attr_accessor :on_failed_sanity_checks, :on_new_version_available, :on_missing_default_urls, :parent_channel, :logging, :middleware
17
+ attr_accessor :on_failed_sanity_checks, :on_new_version_available, :on_missing_default_urls, :parent_channel, :logging, :logger, :middleware
18
18
 
19
19
  DEFAULT_LOGGING = proc { "[#{session_id}] #{operation_counter.magenta} #{reflex_info.green} -> #{selector.cyan} via #{mode} Morph (#{operation.yellow})" }
20
20
 
@@ -24,6 +24,7 @@ module StimulusReflex
24
24
  @on_missing_default_urls = :warn
25
25
  @parent_channel = "ApplicationCable::Channel"
26
26
  @logging = DEFAULT_LOGGING
27
+ @logger = Rails.logger
27
28
  @middleware = ActionDispatch::MiddlewareStack.new
28
29
  end
29
30
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stimulus_reflex/utils/attribute_builder"
4
+
5
+ class StimulusReflex::Dataset < OpenStruct
6
+ include StimulusReflex::AttributeBuilder
7
+
8
+ attr_accessor :attrs, :data_attrs
9
+
10
+ def initialize(data = {})
11
+ datasets = data["dataset"] || {}
12
+ regular_dataset = datasets["dataset"] || {}
13
+ @attrs = build_data_attrs(regular_dataset, datasets["datasetAll"] || {})
14
+ @data_attrs = @attrs.transform_keys { |key| key.delete_prefix "data-" }
15
+
16
+ super build_underscored(@data_attrs)
17
+ end
18
+
19
+ def signed
20
+ @signed ||= ->(accessor) { GlobalID::Locator.locate_signed(self[accessor]) }
21
+ end
22
+
23
+ def unsigned
24
+ @unsigned ||= ->(accessor) { GlobalID::Locator.locate(self[accessor]) }
25
+ end
26
+
27
+ def boolean
28
+ @boolean ||= ->(accessor) { ActiveModel::Type::Boolean.new.cast(self[accessor]) || self[accessor].blank? }
29
+ end
30
+
31
+ def numeric
32
+ @numeric ||= ->(accessor) { Float(self[accessor]) }
33
+ end
34
+ end
@@ -1,66 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class StimulusReflex::Element < OpenStruct
4
- attr_reader :attrs, :data_attrs, :inner_html, :text_content
3
+ require "stimulus_reflex/dataset"
4
+ require "stimulus_reflex/utils/attribute_builder"
5
5
 
6
- def initialize(data = {})
7
- @attrs = HashWithIndifferentAccess.new(data["attrs"] || {})
8
- @inner_html = data["inner_html"]
9
- @text_content = data["text_content"]
6
+ class StimulusReflex::Element < OpenStruct
7
+ include StimulusReflex::AttributeBuilder
10
8
 
11
- datasets = data["dataset"] || {}
12
- regular_dataset = datasets["dataset"] || {}
13
- @data_attrs = build_data_attrs(regular_dataset, datasets["datasetAll"] || {})
9
+ attr_reader :attrs, :dataset
14
10
 
15
- super build_underscored(all_attributes)
11
+ alias_method :data_attributes, :dataset
16
12
 
17
- @data_attrs.transform_keys! { |key| key.delete_prefix "data-" }
18
- end
13
+ delegate :signed, :unsigned, :numeric, :boolean, :data_attrs, to: :dataset
19
14
 
20
- def signed
21
- @signed ||= ->(accessor) { GlobalID::Locator.locate_signed(dataset[accessor]) }
22
- end
15
+ def initialize(data = {})
16
+ @attrs = HashWithIndifferentAccess.new(data["attrs"] || {})
17
+ @dataset = StimulusReflex::Dataset.new(data)
23
18
 
24
- def unsigned
25
- @unsigned ||= ->(accessor) { GlobalID::Locator.locate(dataset[accessor]) }
19
+ all_attributes = @attrs.merge(@dataset.attrs)
20
+ super build_underscored(all_attributes)
26
21
  end
27
22
 
28
23
  def attributes
29
24
  @attributes ||= OpenStruct.new(build_underscored(attrs))
30
25
  end
31
26
 
32
- def dataset
33
- @dataset ||= OpenStruct.new(build_underscored(data_attrs))
34
- end
35
-
36
27
  def to_dom_id
37
28
  raise NoIDError.new "The element `morph` is called on must have a valid DOM ID" if id.blank?
38
29
 
39
30
  "##{id}"
40
31
  end
41
-
42
- alias_method :data_attributes, :dataset
43
-
44
- private
45
-
46
- def all_attributes
47
- @attrs.merge(@data_attrs)
48
- end
49
-
50
- def build_data_attrs(dataset, dataset_all)
51
- dataset_all.transform_keys! { |key| "data-#{key.delete_prefix("data-").pluralize}" }
52
-
53
- dataset.each { |key, value| dataset_all[key]&.prepend(value) }
54
-
55
- data_attrs = dataset.merge(dataset_all)
56
-
57
- HashWithIndifferentAccess.new(data_attrs || {})
58
- end
59
-
60
- def build_underscored(attrs)
61
- attrs.merge(attrs.transform_keys(&:underscore))
62
- end
63
-
64
- class NoIDError < StandardError
65
- end
66
32
  end
@@ -8,14 +8,14 @@ class StimulusReflex::Reflex
8
8
  include ActionView::Helpers::TagHelper
9
9
  include CableReady::Identifiable
10
10
 
11
- attr_accessor :payload
11
+ attr_accessor :payload, :headers
12
12
  attr_reader :cable_ready, :channel, :url, :element, :selectors, :method_name, :broadcaster, :client_attributes, :logger
13
13
 
14
14
  alias_method :action_name, :method_name # for compatibility with controller libraries like Pundit that expect an action name
15
15
 
16
16
  delegate :connection, :stream_name, to: :channel
17
17
  delegate :controller_class, :flash, :session, to: :request
18
- delegate :broadcast, :broadcast_message, to: :broadcaster
18
+ delegate :broadcast, :halted, :error, to: :broadcaster
19
19
  delegate :reflex_id, :tab_id, :reflex_controller, :xpath_controller, :xpath_element, :permanent_attribute_name, to: :client_attributes
20
20
 
21
21
  def initialize(channel, url: nil, element: nil, selectors: [], method_name: nil, params: {}, client_attributes: {})
@@ -41,6 +41,7 @@ class StimulusReflex::Reflex
41
41
  @client_attributes = ClientAttributes.new(client_attributes)
42
42
  @cable_ready = StimulusReflex::CableReadyChannels.new(stream_name, reflex_id)
43
43
  @payload = {}
44
+ @headers = {}
44
45
  self.params
45
46
  end
46
47
 
@@ -97,6 +98,7 @@ class StimulusReflex::Reflex
97
98
 
98
99
  def controller
99
100
  @controller ||= controller_class.new.tap do |c|
101
+ request.headers.merge!(headers)
100
102
  c.instance_variable_set :@stimulus_reflex, true
101
103
  c.set_request! request
102
104
  c.set_response! controller_class.make_response!(request)
@@ -111,6 +113,9 @@ class StimulusReflex::Reflex
111
113
  end
112
114
 
113
115
  def render(*args)
116
+ options = args.extract_options!
117
+ (options[:locals] ||= {}).reverse_merge!(params: params)
118
+ args << options.reverse_merge(layout: false)
114
119
  controller_class.renderer.new(connection.env.merge("SCRIPT_NAME" => "")).render(*args)
115
120
  end
116
121
 
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusReflex::AttributeBuilder
4
+ def build_data_attrs(dataset, dataset_all)
5
+ dataset_all.transform_keys! { |key| "data-#{key.delete_prefix("data-").pluralize}" }
6
+
7
+ dataset.each { |key, value| dataset_all[key]&.prepend(value) }
8
+
9
+ data_attrs = dataset.merge(dataset_all)
10
+
11
+ HashWithIndifferentAccess.new(data_attrs || {})
12
+ end
13
+
14
+ def build_underscored(attrs)
15
+ attrs.merge(attrs.transform_keys(&:underscore))
16
+ end
17
+ end
@@ -2,18 +2,22 @@
2
2
 
3
3
  module StimulusReflex
4
4
  class Logger
5
+ attr_reader :logger
5
6
  attr_accessor :reflex, :current_operation
6
7
 
8
+ delegate :debug, :info, :warn, :error, :fatal, :unknown, to: :logger
9
+
7
10
  def initialize(reflex)
8
11
  @reflex = reflex
9
12
  @current_operation = 1
13
+ @logger = StimulusReflex.config.logger
10
14
  end
11
15
 
12
- def print
16
+ def log_all_operations
13
17
  return unless config_logging.instance_of?(Proc)
14
18
 
15
19
  reflex.broadcaster.operations.each do
16
- puts instance_eval(&config_logging) + "\e[0m"
20
+ logger.info instance_eval(&config_logging) + "\e[0m"
17
21
  @current_operation += 1
18
22
  end
19
23
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StimulusReflex
4
- VERSION = "3.5.0.pre2"
4
+ VERSION = "3.5.0.pre3"
5
5
  end
@@ -96,7 +96,7 @@ namespace :stimulus_reflex do
96
96
  puts
97
97
  system "bundle exec rails generate stimulus_reflex:initializer"
98
98
  system "bundle exec rails generate cable_ready:initializer"
99
- system "bundle exec rails generate cable_ready:stream_from"
99
+ system "bundle exec rails generate cable_ready:helpers"
100
100
 
101
101
  puts
102
102
  puts "✨ Generating ApplicationReflex class and Stimulus controllers, plus an example Reflex class and controller"
@@ -7,6 +7,5 @@ class StimulusReflex::BroadcasterTest < StimulusReflex::BroadcasterTestCase
7
7
  broadcaster = StimulusReflex::Broadcaster.new(@reflex)
8
8
 
9
9
  assert_raises(NotImplementedError) { broadcaster.broadcast }
10
- assert_raises(NotImplementedError) { broadcaster.broadcast_message(subject: "Test") }
11
10
  end
12
11
  end
@@ -5,6 +5,30 @@ require_relative "../test_helper"
5
5
  class StimulusReflex::BroadcasterTestCase < ActionCable::Channel::TestCase
6
6
  tests StimulusReflex::Channel
7
7
 
8
+ def assert_broadcast_on(stream, data, &block)
9
+ serialized_msg = ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(data))
10
+
11
+ new_messages = broadcasts(stream)
12
+ if block
13
+ old_messages = new_messages
14
+ clear_messages(stream)
15
+
16
+ yield
17
+ new_messages = broadcasts(stream)
18
+ clear_messages(stream)
19
+
20
+ (old_messages + new_messages).each { |m| pubsub_adapter.broadcast(stream, m) }
21
+ end
22
+
23
+ message = new_messages.find { |msg| ActiveSupport::JSON.decode(msg) == serialized_msg }
24
+
25
+ unless message
26
+ puts "\n\n#{ActiveSupport::JSON.decode(new_messages.first)}\n#{data}\n\n"
27
+ end
28
+
29
+ assert message, "No messages sent with #{data} to #{stream}"
30
+ end
31
+
8
32
  setup do
9
33
  stub_connection(session_id: SecureRandom.uuid)
10
34
  def connection.env
@@ -3,35 +3,27 @@
3
3
  require_relative "broadcaster_test_case"
4
4
 
5
5
  class StimulusReflex::NothingBroadcasterTest < StimulusReflex::BroadcasterTestCase
6
- test "broadcasts a server message when called" do
6
+ test "broadcasts a nothing morph when called" do
7
7
  broadcaster = StimulusReflex::NothingBroadcaster.new(@reflex)
8
8
 
9
9
  expected = {
10
10
  "cableReady" => true,
11
- "operations" => {
12
- "dispatchEvent" => [
13
- {
14
- "name" => "stimulus-reflex:server-message",
15
- "detail" => {
16
- "reflexId" => "666",
17
- "payload" => {},
18
- "stimulusReflex" => {
19
- "some" => :data,
20
- "morph" => :nothing,
21
- "serverMessage" => {
22
- "subject" => "nothing",
23
- "body" => nil
24
- }
25
- }
26
- },
27
- "reflexId" => "666"
28
- }
29
- ]
30
- }
11
+ "operations" => [
12
+ {
13
+ "name" => "stimulus-reflex:morph-nothing",
14
+ "payload" => {},
15
+ "stimulusReflex" => {
16
+ "some" => "data",
17
+ "morph" => "nothing"
18
+ },
19
+ "reflexId" => "666",
20
+ "operation" => "dispatchEvent"
21
+ }
22
+ ]
31
23
  }
32
24
 
33
25
  assert_broadcast_on @reflex.stream_name, expected do
34
- broadcaster.broadcast nil, {:some => :data, "reflexId" => "666"}
26
+ broadcaster.broadcast nil, {some: :data}
35
27
  end
36
28
  end
37
29
  end
@@ -20,22 +20,21 @@ class StimulusReflex::PageBroadcasterTest < StimulusReflex::BroadcasterTestCase
20
20
 
21
21
  expected = {
22
22
  "cableReady" => true,
23
- "operations" => {
24
- "morph" => [
25
- {
26
- "selector" => "body",
27
- "html" => "<div>New Content</div><div>Another Content</div>",
28
- "payload" => {},
29
- "childrenOnly" => true,
30
- "permanentAttributeName" => nil,
31
- "stimulusReflex" => {
32
- "some" => :data,
33
- "morph" => :page
34
- },
35
- "reflexId" => "666"
36
- }
37
- ]
38
- }
23
+ "operations" => [
24
+ {
25
+ "selector" => "body",
26
+ "html" => "<div>New Content</div><div>Another Content</div>",
27
+ "payload" => {},
28
+ "childrenOnly" => true,
29
+ "permanentAttributeName" => nil,
30
+ "stimulusReflex" => {
31
+ "some" => :data,
32
+ "morph" => :page
33
+ },
34
+ "reflexId" => "666",
35
+ "operation" => "morph"
36
+ }
37
+ ]
39
38
  }
40
39
 
41
40
  assert_broadcast_on @reflex.stream_name, expected do
@@ -54,22 +53,21 @@ class StimulusReflex::PageBroadcasterTest < StimulusReflex::BroadcasterTestCase
54
53
 
55
54
  expected = {
56
55
  "cableReady" => true,
57
- "operations" => {
58
- "morph" => [
59
- {
60
- "selector" => "#foo",
61
- "html" => "New Content",
62
- "payload" => {},
63
- "childrenOnly" => true,
64
- "permanentAttributeName" => nil,
65
- "stimulusReflex" => {
66
- "some" => :data,
67
- "morph" => :page
68
- },
69
- "reflexId" => "666"
70
- }
71
- ]
72
- }
56
+ "operations" => [
57
+ {
58
+ "selector" => "#foo",
59
+ "html" => "New Content",
60
+ "payload" => {},
61
+ "childrenOnly" => true,
62
+ "permanentAttributeName" => nil,
63
+ "stimulusReflex" => {
64
+ "some" => :data,
65
+ "morph" => :page
66
+ },
67
+ "reflexId" => "666",
68
+ "operation" => "morph"
69
+ }
70
+ ]
73
71
  }
74
72
 
75
73
  assert_broadcast_on @reflex.stream_name, expected do
@@ -10,22 +10,21 @@ module StimulusReflex
10
10
 
11
11
  expected = {
12
12
  "cableReady" => true,
13
- "operations" => {
14
- "morph" => [
15
- {
16
- "selector" => "#foo",
17
- "html" => "<div>bar</div><div>baz</div>",
18
- "payload" => {},
19
- "childrenOnly" => true,
20
- "permanentAttributeName" => nil,
21
- "stimulusReflex" => {
22
- "some" => "data",
23
- "morph" => "selector"
24
- },
25
- "reflexId" => "666"
26
- }
27
- ]
28
- }
13
+ "operations" => [
14
+ {
15
+ "selector" => "#foo",
16
+ "html" => "<div>bar</div><div>baz</div>",
17
+ "payload" => {},
18
+ "childrenOnly" => true,
19
+ "permanentAttributeName" => nil,
20
+ "stimulusReflex" => {
21
+ "some" => "data",
22
+ "morph" => "selector"
23
+ },
24
+ "reflexId" => "666",
25
+ "operation" => "morph"
26
+ }
27
+ ]
29
28
  }
30
29
 
31
30
  assert_broadcast_on @reflex.stream_name, expected do
@@ -39,20 +38,19 @@ module StimulusReflex
39
38
 
40
39
  expected = {
41
40
  "cableReady" => true,
42
- "operations" => {
43
- "innerHtml" => [
44
- {
45
- "selector" => "#foo",
46
- "html" => '<div id="baz"><span>bar</span></div>',
47
- "payload" => {},
48
- "stimulusReflex" => {
49
- "some" => "data",
50
- "morph" => "selector"
51
- },
52
- "reflexId" => "666"
53
- }
54
- ]
55
- }
41
+ "operations" => [
42
+ {
43
+ "selector" => "#foo",
44
+ "html" => '<div id="baz"><span>bar</span></div>',
45
+ "payload" => {},
46
+ "stimulusReflex" => {
47
+ "some" => "data",
48
+ "morph" => "selector"
49
+ },
50
+ "reflexId" => "666",
51
+ "operation" => "innerHtml"
52
+ }
53
+ ]
56
54
  }
57
55
 
58
56
  assert_broadcast_on @reflex.stream_name, expected do
@@ -66,20 +64,19 @@ module StimulusReflex
66
64
 
67
65
  expected = {
68
66
  "cableReady" => true,
69
- "operations" => {
70
- "innerHtml" => [
71
- {
72
- "selector" => "#foo",
73
- "html" => "",
74
- "payload" => {},
75
- "stimulusReflex" => {
76
- "some" => "data",
77
- "morph" => "selector"
78
- },
79
- "reflexId" => "666"
80
- }
81
- ]
82
- }
67
+ "operations" => [
68
+ {
69
+ "selector" => "#foo",
70
+ "html" => "",
71
+ "payload" => {},
72
+ "stimulusReflex" => {
73
+ "some" => "data",
74
+ "morph" => "selector"
75
+ },
76
+ "reflexId" => "666",
77
+ "operation" => "innerHtml"
78
+ }
79
+ ]
83
80
  }
84
81
 
85
82
  assert_broadcast_on @reflex.stream_name, expected do
@@ -93,20 +90,19 @@ module StimulusReflex
93
90
 
94
91
  expected = {
95
92
  "cableReady" => true,
96
- "operations" => {
97
- "innerHtml" => [
98
- {
99
- "selector" => "#foo",
100
- "html" => "",
101
- "payload" => {},
102
- "stimulusReflex" => {
103
- "some" => "data",
104
- "morph" => "selector"
105
- },
106
- "reflexId" => "666"
107
- }
108
- ]
109
- }
93
+ "operations" => [
94
+ {
95
+ "selector" => "#foo",
96
+ "html" => "",
97
+ "payload" => {},
98
+ "stimulusReflex" => {
99
+ "some" => "data",
100
+ "morph" => "selector"
101
+ },
102
+ "reflexId" => "666",
103
+ "operation" => "innerHtml"
104
+ }
105
+ ]
110
106
  }
111
107
 
112
108
  assert_broadcast_on @reflex.stream_name, expected do
@@ -120,20 +116,19 @@ module StimulusReflex
120
116
 
121
117
  expected = {
122
118
  "cableReady" => true,
123
- "operations" => {
124
- "innerHtml" => [
125
- {
126
- "selector" => "#foo",
127
- "html" => "",
128
- "payload" => {},
129
- "stimulusReflex" => {
130
- "some" => "data",
131
- "morph" => "selector"
132
- },
133
- "reflexId" => "666"
134
- }
135
- ]
136
- }
119
+ "operations" => [
120
+ {
121
+ "selector" => "#foo",
122
+ "html" => "",
123
+ "payload" => {},
124
+ "stimulusReflex" => {
125
+ "some" => "data",
126
+ "morph" => "selector"
127
+ },
128
+ "reflexId" => "666",
129
+ "operation" => "innerHtml"
130
+ }
131
+ ]
137
132
  }
138
133
 
139
134
  assert_broadcast_on @reflex.stream_name, expected do
@@ -147,22 +142,21 @@ module StimulusReflex
147
142
 
148
143
  expected = {
149
144
  "cableReady" => true,
150
- "operations" => {
151
- "morph" => [
152
- {
153
- "selector" => "#foo",
154
- "html" => "<div>bar</div><div>baz</div>",
155
- "payload" => {},
156
- "childrenOnly" => true,
157
- "permanentAttributeName" => nil,
158
- "stimulusReflex" => {
159
- "some" => "data",
160
- "morph" => "selector"
161
- },
162
- "reflexId" => "666"
163
- }
164
- ]
165
- }
145
+ "operations" => [
146
+ {
147
+ "selector" => "#foo",
148
+ "html" => "<div>bar</div><div>baz</div>",
149
+ "payload" => {},
150
+ "childrenOnly" => true,
151
+ "permanentAttributeName" => nil,
152
+ "stimulusReflex" => {
153
+ "some" => "data",
154
+ "morph" => "selector"
155
+ },
156
+ "reflexId" => "666",
157
+ "operation" => "morph"
158
+ }
159
+ ]
166
160
  }
167
161
 
168
162
  assert_broadcast_on @reflex.stream_name, expected do