ably 0.6.2 → 0.7.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.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.ruby-version.old +1 -0
  4. data/.travis.yml +0 -2
  5. data/Rakefile +22 -4
  6. data/SPEC.md +1676 -0
  7. data/ably.gemspec +1 -1
  8. data/lib/ably.rb +0 -8
  9. data/lib/ably/auth.rb +54 -46
  10. data/lib/ably/exceptions.rb +19 -5
  11. data/lib/ably/logger.rb +1 -1
  12. data/lib/ably/models/error_info.rb +1 -1
  13. data/lib/ably/models/idiomatic_ruby_wrapper.rb +11 -9
  14. data/lib/ably/models/message.rb +15 -12
  15. data/lib/ably/models/message_encoders/base.rb +6 -5
  16. data/lib/ably/models/message_encoders/base64.rb +1 -0
  17. data/lib/ably/models/message_encoders/cipher.rb +6 -3
  18. data/lib/ably/models/message_encoders/json.rb +1 -0
  19. data/lib/ably/models/message_encoders/utf8.rb +2 -9
  20. data/lib/ably/models/nil_logger.rb +20 -0
  21. data/lib/ably/models/paginated_resource.rb +5 -2
  22. data/lib/ably/models/presence_message.rb +21 -12
  23. data/lib/ably/models/protocol_message.rb +22 -6
  24. data/lib/ably/modules/ably.rb +11 -0
  25. data/lib/ably/modules/async_wrapper.rb +2 -0
  26. data/lib/ably/modules/conversions.rb +23 -3
  27. data/lib/ably/modules/encodeable.rb +2 -1
  28. data/lib/ably/modules/enum.rb +2 -0
  29. data/lib/ably/modules/event_emitter.rb +7 -1
  30. data/lib/ably/modules/event_machine_helpers.rb +2 -0
  31. data/lib/ably/modules/http_helpers.rb +2 -0
  32. data/lib/ably/modules/model_common.rb +12 -2
  33. data/lib/ably/modules/state_emitter.rb +76 -0
  34. data/lib/ably/modules/state_machine.rb +53 -0
  35. data/lib/ably/modules/statesman_monkey_patch.rb +33 -0
  36. data/lib/ably/modules/uses_state_machine.rb +74 -0
  37. data/lib/ably/realtime.rb +4 -2
  38. data/lib/ably/realtime/channel.rb +51 -58
  39. data/lib/ably/realtime/channel/channel_manager.rb +91 -0
  40. data/lib/ably/realtime/channel/channel_state_machine.rb +68 -0
  41. data/lib/ably/realtime/client.rb +70 -26
  42. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +31 -13
  43. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
  44. data/lib/ably/realtime/connection.rb +135 -92
  45. data/lib/ably/realtime/connection/connection_manager.rb +216 -33
  46. data/lib/ably/realtime/connection/connection_state_machine.rb +30 -73
  47. data/lib/ably/realtime/models/nil_channel.rb +10 -1
  48. data/lib/ably/realtime/presence.rb +336 -92
  49. data/lib/ably/rest.rb +2 -2
  50. data/lib/ably/rest/channel.rb +13 -4
  51. data/lib/ably/rest/client.rb +138 -38
  52. data/lib/ably/rest/middleware/logger.rb +24 -3
  53. data/lib/ably/rest/presence.rb +12 -7
  54. data/lib/ably/version.rb +1 -1
  55. data/spec/acceptance/realtime/channel_history_spec.rb +101 -85
  56. data/spec/acceptance/realtime/channel_spec.rb +461 -120
  57. data/spec/acceptance/realtime/client_spec.rb +119 -0
  58. data/spec/acceptance/realtime/connection_failures_spec.rb +499 -0
  59. data/spec/acceptance/realtime/connection_spec.rb +571 -97
  60. data/spec/acceptance/realtime/message_spec.rb +347 -333
  61. data/spec/acceptance/realtime/presence_history_spec.rb +35 -40
  62. data/spec/acceptance/realtime/presence_spec.rb +769 -239
  63. data/spec/acceptance/realtime/stats_spec.rb +14 -22
  64. data/spec/acceptance/realtime/time_spec.rb +16 -20
  65. data/spec/acceptance/rest/auth_spec.rb +425 -364
  66. data/spec/acceptance/rest/base_spec.rb +108 -176
  67. data/spec/acceptance/rest/channel_spec.rb +89 -89
  68. data/spec/acceptance/rest/channels_spec.rb +30 -32
  69. data/spec/acceptance/rest/client_spec.rb +273 -0
  70. data/spec/acceptance/rest/encoders_spec.rb +185 -0
  71. data/spec/acceptance/rest/message_spec.rb +186 -163
  72. data/spec/acceptance/rest/presence_spec.rb +150 -111
  73. data/spec/acceptance/rest/stats_spec.rb +45 -40
  74. data/spec/acceptance/rest/time_spec.rb +8 -10
  75. data/spec/rspec_config.rb +10 -1
  76. data/spec/shared/client_initializer_behaviour.rb +212 -0
  77. data/spec/{support/model_helper.rb → shared/model_behaviour.rb} +6 -6
  78. data/spec/{support/protocol_msgbus_helper.rb → shared/protocol_msgbus_behaviour.rb} +1 -1
  79. data/spec/spec_helper.rb +9 -0
  80. data/spec/support/api_helper.rb +11 -0
  81. data/spec/support/event_machine_helper.rb +101 -3
  82. data/spec/support/markdown_spec_formatter.rb +90 -0
  83. data/spec/support/private_api_formatter.rb +36 -0
  84. data/spec/support/protocol_helper.rb +32 -0
  85. data/spec/support/random_helper.rb +15 -0
  86. data/spec/support/test_app.rb +4 -0
  87. data/spec/unit/auth_spec.rb +68 -0
  88. data/spec/unit/logger_spec.rb +77 -66
  89. data/spec/unit/models/error_info_spec.rb +1 -1
  90. data/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +2 -3
  91. data/spec/unit/models/message_encoders/base64_spec.rb +2 -2
  92. data/spec/unit/models/message_encoders/cipher_spec.rb +2 -2
  93. data/spec/unit/models/message_encoders/utf8_spec.rb +2 -46
  94. data/spec/unit/models/message_spec.rb +160 -15
  95. data/spec/unit/models/paginated_resource_spec.rb +29 -27
  96. data/spec/unit/models/presence_message_spec.rb +163 -20
  97. data/spec/unit/models/protocol_message_spec.rb +43 -8
  98. data/spec/unit/modules/async_wrapper_spec.rb +2 -3
  99. data/spec/unit/modules/conversions_spec.rb +1 -1
  100. data/spec/unit/modules/enum_spec.rb +2 -3
  101. data/spec/unit/modules/event_emitter_spec.rb +62 -5
  102. data/spec/unit/modules/state_emitter_spec.rb +283 -0
  103. data/spec/unit/realtime/channel_spec.rb +107 -2
  104. data/spec/unit/realtime/channels_spec.rb +1 -0
  105. data/spec/unit/realtime/client_spec.rb +8 -48
  106. data/spec/unit/realtime/connection_spec.rb +3 -3
  107. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +2 -2
  108. data/spec/unit/realtime/presence_spec.rb +13 -4
  109. data/spec/unit/realtime/realtime_spec.rb +0 -11
  110. data/spec/unit/realtime/websocket_transport_spec.rb +2 -2
  111. data/spec/unit/rest/channel_spec.rb +109 -0
  112. data/spec/unit/rest/channels_spec.rb +4 -3
  113. data/spec/unit/rest/client_spec.rb +30 -125
  114. data/spec/unit/rest/rest_spec.rb +10 -0
  115. data/spec/unit/util/crypto_spec.rb +10 -5
  116. data/spec/unit/util/pub_sub_spec.rb +5 -5
  117. metadata +44 -12
  118. data/spec/integration/modules/state_emitter_spec.rb +0 -80
  119. data/spec/integration/rest/auth.rb +0 -9
@@ -1,5 +1,6 @@
1
+ # encoding: utf-8
1
2
  require 'spec_helper'
2
- require 'support/model_helper'
3
+ require 'shared/model_behaviour'
3
4
 
4
5
  describe Ably::Models::ProtocolMessage do
5
6
  include Ably::Modules::Conversions
@@ -16,7 +17,7 @@ describe Ably::Models::ProtocolMessage do
16
17
  let(:model_args) { [] }
17
18
  end
18
19
 
19
- context 'initializer action coercion' do
20
+ context 'initializer action coercion', :api_private do
20
21
  it 'ignores actions that are Integers' do
21
22
  protocol_message = subject.new(action: 14)
22
23
  expect(protocol_message.hash[:action]).to eql(14)
@@ -38,9 +39,9 @@ describe Ably::Models::ProtocolMessage do
38
39
  end
39
40
 
40
41
  context 'attributes' do
41
- let(:unique_value) { SecureRandom.hex }
42
+ let(:unique_value) { random_str }
42
43
 
43
- context 'Java naming' do
44
+ context 'Java naming', :api_private do
44
45
  let(:protocol_message) { new_protocol_message(channelSerial: unique_value) }
45
46
 
46
47
  it 'converts the attribute to ruby symbol naming convention' do
@@ -48,7 +49,7 @@ describe Ably::Models::ProtocolMessage do
48
49
  end
49
50
  end
50
51
 
51
- context '#action' do
52
+ context '#action', :api_private do
52
53
  let(:protocol_message) { new_protocol_message(action: 14) }
53
54
 
54
55
  it 'returns an Enum that behaves like a symbol' do
@@ -70,7 +71,7 @@ describe Ably::Models::ProtocolMessage do
70
71
 
71
72
  context '#timestamp' do
72
73
  let(:protocol_message) { new_protocol_message(timestamp: as_since_epoch(Time.now)) }
73
- it 'retrieves attribute :timestamp' do
74
+ it 'retrieves attribute :timestamp as Time object' do
74
75
  expect(protocol_message.timestamp).to be_a(Time)
75
76
  expect(protocol_message.timestamp.to_i).to be_within(1).of(Time.now.to_i)
76
77
  end
@@ -133,6 +134,40 @@ describe Ably::Models::ProtocolMessage do
133
134
  end
134
135
  end
135
136
 
137
+ context '#flags' do
138
+ context 'when nil' do
139
+ let(:protocol_message) { new_protocol_message({}) }
140
+
141
+ it 'is zero' do
142
+ expect(protocol_message.flags).to eql(0)
143
+ end
144
+ end
145
+
146
+ context 'when numeric' do
147
+ let(:protocol_message) { new_protocol_message(flags: '25') }
148
+
149
+ it 'is an Integer' do
150
+ expect(protocol_message.flags).to eql(25)
151
+ end
152
+ end
153
+
154
+ context 'when has_presence' do
155
+ let(:protocol_message) { new_protocol_message(flags: 1) }
156
+
157
+ it '#has_presence_flag? is true' do
158
+ expect(protocol_message.has_presence_flag?).to be_truthy
159
+ end
160
+ end
161
+
162
+ context 'when has another future flag' do
163
+ let(:protocol_message) { new_protocol_message(flags: 2) }
164
+
165
+ it '#has_presence_flag? is false' do
166
+ expect(protocol_message.has_presence_flag?).to be_falsey
167
+ end
168
+ end
169
+ end
170
+
136
171
  context '#has_connection_serial?' do
137
172
  context 'without connection_serial' do
138
173
  let(:protocol_message) { new_protocol_message({}) }
@@ -231,7 +266,7 @@ describe Ably::Models::ProtocolMessage do
231
266
  end
232
267
  end
233
268
 
234
- context '#to_json' do
269
+ context '#to_json', :api_private do
235
270
  let(:json_object) { JSON.parse(model.to_json) }
236
271
  let(:message) { { 'name' => 'event', 'clientId' => 'joe', 'timestamp' => as_since_epoch(Time.now) } }
237
272
  let(:attached_action) { Ably::Models::ProtocolMessage::ACTION.Attached }
@@ -266,7 +301,7 @@ describe Ably::Models::ProtocolMessage do
266
301
  end
267
302
  end
268
303
 
269
- context '#to_msgpack' do
304
+ context '#to_msgpack', :api_private do
270
305
  let(:model) { new_protocol_message({ :connectionSerial => 'unique', messages: [message] }) }
271
306
  let(:message) { { 'name' => 'event', 'clientId' => 'joe', 'timestamp' => as_since_epoch(Time.now) } }
272
307
  let(:packed) { model.to_msgpack }
@@ -1,7 +1,6 @@
1
1
  require 'spec_helper'
2
- require 'securerandom'
3
2
 
4
- describe Ably::Modules::AsyncWrapper do
3
+ describe Ably::Modules::AsyncWrapper, :api_private do
5
4
  include RSpec::EventMachine
6
5
 
7
6
  let(:class_with_module) do
@@ -18,7 +17,7 @@ describe Ably::Modules::AsyncWrapper do
18
17
  end
19
18
  end
20
19
  let(:subject) { class_with_module.new }
21
- let(:result) { SecureRandom.hex }
20
+ let(:result) { random_str }
22
21
  let(:sleep_time) { 0.1 }
23
22
 
24
23
  before do
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Ably::Modules::Conversions do
3
+ describe Ably::Modules::Conversions, :api_private do
4
4
  let(:class_with_module) { Class.new do; include Ably::Modules::Conversions; end }
5
5
  let(:subject) { class_with_module.new }
6
6
  before do
@@ -1,7 +1,6 @@
1
1
  require 'spec_helper'
2
- require 'securerandom'
3
2
 
4
- describe Ably::Modules::Enum do
3
+ describe Ably::Modules::Enum, :api_private do
5
4
  class ExampleClassWithEnum
6
5
  extend Ably::Modules::Enum
7
6
  ENUMEXAMPLE = ruby_enum('ENUMEXAMPLE', :value_zero, 'value_1', :value_snake_case_2, :SentenceCase)
@@ -92,7 +91,7 @@ describe Ably::Modules::Enum do
92
91
  end
93
92
 
94
93
  context '#[]' do
95
- let(:argument) { SecureRandom.hex }
94
+ let(:argument) { random_str }
96
95
  before do
97
96
  expect(subject).to receive(:get).with(argument).once.and_return(true)
98
97
  end
@@ -14,8 +14,8 @@ describe Ably::Modules::EventEmitter do
14
14
 
15
15
  subject { klass.new }
16
16
 
17
- context 'event fan out' do
18
- specify do
17
+ context '#trigger event fan out' do
18
+ it 'should emit an event for any number of subscribers' do
19
19
  2.times do
20
20
  subject.on(:message) { |msg| obj.received_message msg }
21
21
  end
@@ -33,7 +33,7 @@ describe Ably::Modules::EventEmitter do
33
33
  subject.trigger 'valid', msg
34
34
  end
35
35
 
36
- context 'with coercion' do
36
+ context 'with coercion', :api_private do
37
37
  let(:options) do
38
38
  { coerce_into: Proc.new { |event| String(event) } }
39
39
  end
@@ -46,7 +46,7 @@ describe Ably::Modules::EventEmitter do
46
46
  end
47
47
  end
48
48
 
49
- context 'without coercion' do
49
+ context 'without coercion', :api_private do
50
50
  it 'only matches event names on type matches' do
51
51
  subject.on('valid') { |msg| obj.received_message msg }
52
52
 
@@ -55,7 +55,7 @@ describe Ably::Modules::EventEmitter do
55
55
  end
56
56
  end
57
57
 
58
- context 'subscribe to multiple events' do
58
+ context '#on subscribe to multiple events' do
59
59
  it 'with the same block' do
60
60
  subject.on(:click, :hover) { |msg| obj.received_message msg }
61
61
 
@@ -65,6 +65,63 @@ describe Ably::Modules::EventEmitter do
65
65
  subject.trigger :hover, msg
66
66
  end
67
67
  end
68
+
69
+ context 'event callback changes within the callback block' do
70
+ context 'when new event callbacks are added' do
71
+ before do
72
+ 2.times do
73
+ subject.on(:message) do |msg|
74
+ obj.received_message msg
75
+ subject.on(:message) do |msg|
76
+ obj.received_message_from_new_callbacks msg
77
+ end
78
+ end
79
+ end
80
+ allow(obj).to receive(:received_message)
81
+ end
82
+
83
+ it 'is unaffected and processes the prior event callbacks once' do
84
+ expect(obj).to receive(:received_message).with(msg).twice
85
+ expect(obj).to_not receive(:received_message_from_new_callbacks).with(msg)
86
+ subject.trigger :message, msg
87
+ end
88
+
89
+ it 'adds them for the next emitted event' do
90
+ expect(obj).to receive(:received_message_from_new_callbacks).with(msg).twice
91
+
92
+ # New callbacks are added in this trigger
93
+ subject.trigger :message, msg
94
+
95
+ # New callbacks are now called with second event emitted
96
+ subject.trigger :message, msg
97
+ end
98
+ end
99
+
100
+ context 'when callbacks are removed' do
101
+ before do
102
+ 2.times do
103
+ subject.once(:message) do |msg|
104
+ obj.received_message msg
105
+ subject.off
106
+ end
107
+ end
108
+ end
109
+
110
+ it 'is unaffected and processes the prior event callbacks once' do
111
+ expect(obj).to receive(:received_message).with(msg).twice
112
+ subject.trigger :message, msg
113
+ end
114
+
115
+ it 'removes them for the next emitted event' do
116
+ expect(obj).to receive(:received_message).with(msg).twice
117
+
118
+ # Callbacks are removed in this trigger
119
+ subject.trigger :message, msg
120
+ # No callbacks should exist now
121
+ subject.trigger :message, msg
122
+ end
123
+ end
124
+ end
68
125
  end
69
126
 
70
127
  context '#once' do
@@ -0,0 +1,283 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ably::Modules::StateEmitter do
4
+ class ExampleStateWithEventEmitter
5
+ include Ably::Modules::EventEmitter
6
+ extend Ably::Modules::Enum
7
+
8
+ STATE = ruby_enum('STATE',
9
+ :initializing,
10
+ :connecting,
11
+ :connected,
12
+ :disconnected
13
+ )
14
+ include Ably::Modules::StateEmitter
15
+
16
+ def initialize
17
+ @state = :initializing
18
+ end
19
+ end
20
+
21
+ let(:initial_state) { :initializing }
22
+
23
+ subject { ExampleStateWithEventEmitter.new }
24
+
25
+ specify '#state returns current state' do
26
+ expect(subject.state).to eq(:initializing)
27
+ end
28
+
29
+ specify '#state= sets current state' do
30
+ expect { subject.state = :connecting }.to change { subject.state }.to(:connecting)
31
+ end
32
+
33
+ specify '#change_state sets current state' do
34
+ expect { subject.change_state :connecting }.to change { subject.state }.to(:connecting)
35
+ end
36
+
37
+ context '#change_state with arguments' do
38
+ let(:args) { [5,3,1] }
39
+ let(:callback_status) { { called: false } }
40
+
41
+ it 'passes the arguments through to the triggered callback' do
42
+ subject.on(:connecting) do |*callback_args|
43
+ expect(callback_args).to eql(args)
44
+ callback_status[:called] = true
45
+ end
46
+ expect { subject.change_state :connecting, *args }.to change { subject.state }.to(:connecting)
47
+ expect(callback_status).to eql(called: true)
48
+ end
49
+ end
50
+
51
+ context '#state?' do
52
+ it 'returns true if state matches' do
53
+ expect(subject.state?(initial_state)).to eql(true)
54
+ end
55
+
56
+ it 'returns false if state does not match' do
57
+ expect(subject.state?(:connecting)).to eql(false)
58
+ end
59
+
60
+ context 'and convenience predicates for states' do
61
+ it 'returns true for #initializing? if state matches' do
62
+ expect(subject.initializing?).to eql(true)
63
+ end
64
+
65
+ it 'returns false for #connecting? if state does not match' do
66
+ expect(subject.connecting?).to eql(false)
67
+ end
68
+ end
69
+ end
70
+
71
+ context '#state STATE coercion', :api_private do
72
+ it 'allows valid STATE values' do
73
+ expect { subject.state = :connected }.to_not raise_error
74
+ end
75
+
76
+ it 'prevents invalid STATE values' do
77
+ expect { subject.state = :invalid }.to raise_error KeyError
78
+ end
79
+ end
80
+
81
+ context '#once_or_if', :api_private do
82
+ context 'without :else option block' do
83
+ let(:block_calls) { [] }
84
+ let(:block) do
85
+ proc do
86
+ block_calls << Time.now
87
+ end
88
+ end
89
+
90
+ it 'calls the block if in the provided state' do
91
+ subject.once_or_if initial_state, &block
92
+ expect(block_calls.count).to eql(1)
93
+ end
94
+
95
+ it 'calls the block when the state is reached' do
96
+ subject.once_or_if :connected, &block
97
+ expect(block_calls.count).to eql(0)
98
+
99
+ subject.change_state :connected
100
+ expect(block_calls.count).to eql(1)
101
+ end
102
+
103
+ it 'calls the block only once' do
104
+ subject.once_or_if :connected, &block
105
+ 3.times do
106
+ subject.change_state :connected
107
+ subject.change_state :connecting
108
+ end
109
+ expect(block_calls.count).to eql(1)
110
+ end
111
+ end
112
+
113
+ context 'with an array of targets' do
114
+ let(:block_calls) { [] }
115
+ let(:block) do
116
+ proc do
117
+ block_calls << Time.now
118
+ end
119
+ end
120
+
121
+ it 'calls the block if in the provided state' do
122
+ subject.once_or_if [initial_state, :connecting], &block
123
+ expect(block_calls.count).to eql(1)
124
+ end
125
+
126
+ it 'calls the block when one of the states is reached' do
127
+ subject.once_or_if [:connecting, :connected], &block
128
+ expect(block_calls.count).to eql(0)
129
+
130
+ subject.change_state :connected
131
+ expect(block_calls.count).to eql(1)
132
+ end
133
+
134
+ it 'calls the block only once' do
135
+ subject.once_or_if [:connecting, :connected], &block
136
+ expect(block_calls.count).to eql(0)
137
+
138
+ 3.times do
139
+ subject.change_state :connected
140
+ subject.change_state :connecting
141
+ end
142
+ expect(block_calls.count).to eql(1)
143
+ end
144
+
145
+ it 'does not remove all blocks on success' do
146
+ allow(subject).to receive(:off) do |&block|
147
+ raise 'Should not receive a nil block' if block.nil?
148
+ end
149
+
150
+ subject.once_or_if(:connected) { }
151
+ subject.change_state :connected
152
+ end
153
+ end
154
+
155
+ context 'with :else option block', :api_private do
156
+ let(:success_calls) { [] }
157
+ let(:success_block) do
158
+ proc do
159
+ success_calls << Time.now
160
+ end
161
+ end
162
+
163
+ let(:failure_calls) { [] }
164
+ let(:failure_block) do
165
+ proc do |*args|
166
+ failure_calls << args
167
+ end
168
+ end
169
+
170
+ let(:target_state) { :connected }
171
+
172
+ before do
173
+ subject.once_or_if target_state, else: failure_block, &success_block
174
+ end
175
+
176
+ context 'blocks' do
177
+ specify 'are not called if the state does not change' do
178
+ subject.change_state initial_state
179
+ expect(success_calls.count).to eql(0)
180
+ expect(failure_calls.count).to eql(0)
181
+ end
182
+ end
183
+
184
+ context 'success block' do
185
+ it 'is called once target_state is reached' do
186
+ subject.change_state target_state
187
+ expect(success_calls.count).to eql(1)
188
+ end
189
+
190
+ it 'is never called again once target_state is reached' do
191
+ subject.change_state target_state
192
+ subject.change_state :connecting
193
+ subject.change_state target_state
194
+ expect(success_calls.count).to eql(1)
195
+ end
196
+
197
+ it 'is never called after failure block was called' do
198
+ subject.change_state :connecting
199
+ subject.change_state target_state
200
+ expect(success_calls.count).to eql(0)
201
+ expect(failure_calls.count).to eql(1)
202
+ end
203
+ end
204
+
205
+ context 'failure block' do
206
+ it 'is called once a state other than target_state is reached' do
207
+ subject.change_state :connecting
208
+ expect(failure_calls.count).to eql(1)
209
+ end
210
+
211
+ it 'is never called again once the block has been called previously' do
212
+ subject.change_state :connecting
213
+ subject.change_state target_state
214
+ subject.change_state :connecting
215
+ expect(failure_calls.count).to eql(1)
216
+ end
217
+
218
+ it 'is never called after success block was called' do
219
+ subject.change_state target_state
220
+ subject.change_state :connecting
221
+ expect(failure_calls.count).to eql(0)
222
+ expect(success_calls.count).to eql(1)
223
+ end
224
+
225
+ it 'has arguments from the error state' do
226
+ subject.change_state :disconnected, 1, 2
227
+ expect(failure_calls.count).to eql(1)
228
+ expect(failure_calls.first).to contain_exactly(1, 2)
229
+ end
230
+ end
231
+ end
232
+
233
+ context 'state change arguments' do
234
+ let(:arguments) { [1,2,3] }
235
+
236
+ specify 'are passed to success blocks' do
237
+ subject.once_or_if(:connected) do |*arguments|
238
+ expect(arguments).to eql(arguments)
239
+ end
240
+ subject.change_state :connected, *arguments
241
+ end
242
+
243
+ specify 'are passed to else blocks' do
244
+ else_block = proc { |arguments| expect(arguments).to eql(arguments) }
245
+ subject.once_or_if(:connected, else: else_block) do
246
+ raise 'Success should not be called'
247
+ end
248
+ subject.change_state :connecting, *arguments
249
+ end
250
+ end
251
+ end
252
+
253
+ context '#once_state_changed', :api_private do
254
+ let(:block_calls) { [] }
255
+ let(:block) do
256
+ proc do |*args|
257
+ block_calls << args
258
+ end
259
+ end
260
+
261
+ it 'is not called if the state does not change' do
262
+ subject.once_state_changed &block
263
+ subject.change_state initial_state
264
+ expect(block_calls.count).to eql(0)
265
+ end
266
+
267
+ it 'calls the block for any state change once' do
268
+ subject.once_state_changed &block
269
+ 3.times do
270
+ subject.change_state :connected
271
+ subject.change_state :connecting
272
+ end
273
+ expect(block_calls.count).to eql(1)
274
+ end
275
+
276
+ it 'emits arguments to the block' do
277
+ subject.once_state_changed &block
278
+ subject.change_state :connected, 1, 2
279
+ expect(block_calls.count).to eql(1)
280
+ expect(block_calls.first).to contain_exactly(1, 2)
281
+ end
282
+ end
283
+ end