call_center 0.0.2

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.
@@ -0,0 +1,21 @@
1
+ # From Active Support
2
+ class Object
3
+ unless defined? instance_exec
4
+ def instance_exec(*args, &block)
5
+ begin
6
+ old_critical, Thread.critical = Thread.critical, true
7
+ n = 0
8
+ n += 1 while respond_to?(method_name = "__instance_exec#{n}")
9
+ InstanceExecMethods.module_eval { define_method(method_name, &block) }
10
+ ensure
11
+ Thread.critical = old_critical
12
+ end
13
+
14
+ begin
15
+ send(method_name, *args)
16
+ ensure
17
+ InstanceExecMethods.module_eval { remove_method(method_name) } rescue nil
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ # Extension for StateMachine::Machine to store and provide render blocks
2
+ class StateMachine::Machine
3
+ attr_accessor :render_blocks
4
+ attr_accessor :flow_to_blocks
5
+
6
+ def on_render(state_name, &blk)
7
+ @render_blocks ||= {}
8
+ @render_blocks[state_name] = blk
9
+ end
10
+
11
+ def on_flow_to(state_name, &blk)
12
+ @flow_to_blocks ||= {}
13
+ @flow_to_blocks[state_name] = blk
14
+ end
15
+ end
16
+
17
+ # Extension for StateMachine::AlternateMachine to provide render blocks inside a state definition
18
+ class StateMachine::AlternateMachine
19
+ def on_render(state_name = nil, &blk)
20
+ if @from_state
21
+ @queued_sends << [[:on_render, @from_state], blk]
22
+ else
23
+ @queued_sends << [[:on_render, state_name], blk]
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,69 @@
1
+ module CallCenter
2
+ module Test
3
+ module DSL
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ base.class_eval do
7
+ def response_from_page_or_rjs_with_body
8
+ HTML::Document.new(CGI.unescapeHTML(@body)).root
9
+ end
10
+ alias_method_chain :response_from_page_or_rjs, :body
11
+ end
12
+ end
13
+
14
+ def body(text, debug = false)
15
+ puts text if debug
16
+ @body = text
17
+ end
18
+
19
+ module ClassMethods
20
+ def should_flow(options, &block)
21
+ event = options.delete(:on)
22
+ setup_block = options.delete(:when)
23
+ setup_block_line = setup_block.to_s.match(/.*@(.*):([0-9]+)>/)[2] if setup_block
24
+ state_field = options.delete(:state) || :state
25
+ from, to = options.to_a.first
26
+ description = ":#{from} => :#{to} via #{event}!#{setup_block_line.present? ? " when:#{setup_block_line}" : nil}"
27
+ context "" do
28
+ should "transition #{description}" do
29
+ self.instance_eval(&setup_block) if setup_block
30
+ @call.send(:"#{state_field}=", from.to_s)
31
+ @call.send(:"#{event}")
32
+ assert_equal to, @call.send(:"#{state_field}_name"), "status should be :#{to}, not :#{@call.send(state_field)}"
33
+ end
34
+
35
+ if block.present?
36
+ context "#{description} and :#{to}" do
37
+ setup do
38
+ self.instance_eval(&setup_block) if setup_block
39
+ @call.send(:"#{state_field}=", from.to_s)
40
+ @call.send(:"#{event}")
41
+ body(@call.render) if @call.respond_to?(:render)
42
+ end
43
+
44
+ self.instance_eval(&block)
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ def should_also(&block)
51
+ line = block.to_s.match(/.*@(.*):([0-9]+)>/)[2]
52
+ should "also satisfy block on line #{line}" do
53
+ self.instance_eval(&block)
54
+ end
55
+ end
56
+ alias_method :and_also, :should_also
57
+
58
+ def should_render(&block)
59
+ line = block.to_s.match(/.*@(.*):([0-9]+)>/)[2]
60
+ should "render selector on line #{line}" do
61
+ args = [self.instance_eval(&block)].flatten
62
+ assert_select *args
63
+ end
64
+ end
65
+ alias_method :and_render, :should_render
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,261 @@
1
+ require 'helper'
2
+
3
+ require 'call_center/test/dsl'
4
+
5
+ require 'test/examples/legacy_call'
6
+ require 'test/examples/call'
7
+ require 'test/examples/non_standard_call'
8
+ require 'test/examples/multiple_flow_call'
9
+
10
+ class CallCenterTest < Test::Unit::TestCase
11
+ include CallCenter::Test::DSL
12
+
13
+ [:call, :legacy_call].each do |call_type|
14
+ context "#{call_type.to_s.gsub('_', ' ')} workflow" do
15
+ setup do
16
+ klass = call_type.to_s.gsub('_', ' ').titleize.gsub(' ', '').constantize
17
+ @call = klass.new
18
+ @call.stubs(:notify)
19
+ @call.stubs(:flow_url).returns('the_flow')
20
+ end
21
+
22
+ context "agents available" do
23
+ setup do
24
+ @call.stubs(:agents_available?).returns(true)
25
+ end
26
+
27
+ should "transition to routing" do
28
+ @call.incoming_call!
29
+ assert_equal 'routing', @call.state
30
+ end
31
+ end
32
+
33
+ context "no agents available "do
34
+ setup do
35
+ @call.stubs(:agents_available?).returns(false)
36
+ end
37
+
38
+ should "transition to voicemail" do
39
+ @call.incoming_call!
40
+ assert_equal 'voicemail', @call.state
41
+ end
42
+ end
43
+
44
+ context "in voicemail" do
45
+ setup do
46
+ @call.stubs(:agents_available?).returns(false)
47
+ @call.incoming_call!
48
+ end
49
+
50
+ context "and customer hangs up" do
51
+ should "transition to voicemail_completed" do
52
+ @call.customer_hangs_up!
53
+ assert @call.voicemail_completed?
54
+ end
55
+ end
56
+ end
57
+
58
+ context "something crazy happens" do
59
+ # It's going to try to call the after transition method, but since it doesn't exist...
60
+ should "be ok" do
61
+ @call.something_crazy_happens!
62
+ end
63
+ end
64
+
65
+ context "cancelled" do
66
+ should "stay in cancelled" do
67
+ @call.stubs(:cancelled)
68
+ @call.state = 'cancelled'
69
+
70
+ @call.customer_hangs_up!
71
+
72
+ assert @call.cancelled?
73
+ assert_received(@call, :cancelled) { |e| e.never }
74
+ end
75
+ end
76
+
77
+ context "using test DSL:" do
78
+ should_flow :on => :incoming_call, :initial => :routing, :when => Proc.new {
79
+ @call.stubs(:agents_available?).returns(true)
80
+ }
81
+
82
+ should_flow :on => :incoming_call, :initial => :voicemail, :when => Proc.new {
83
+ @call.stubs(:agents_available?).returns(false)
84
+ } do
85
+ should_flow :on => :customer_hangs_up, :voicemail => :voicemail_completed
86
+ end
87
+
88
+ should_flow :on => :something_crazy_happens, :initial => :uh_oh
89
+
90
+ should_flow :on => :customer_hangs_up, :cancelled => :cancelled do
91
+ should_also { assert_received(@call, :cancelled) { |e| e.never } }
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ context "call" do
98
+ setup do
99
+ @call = Call.new
100
+ end
101
+
102
+ should "render xml for initial state" do
103
+ @call.expects(:notify).with(:rendering_initial)
104
+ body @call.render
105
+ assert_select "Response>Say", "Hello World"
106
+ end
107
+
108
+ should "render xml for voicemail state" do
109
+ @call.stubs(:agents_available?).returns(false)
110
+ @call.incoming_call!
111
+ @call.expects(:notify).with(:rendering_voicemail)
112
+ @call.expects(:flow_url).with(:voicemail_complete).returns('the_flow')
113
+
114
+ body @call.render
115
+ assert_select "Response>Say"
116
+ assert_select "Response>Record[action=the_flow]"
117
+ end
118
+
119
+ should "render noop when no render block" do
120
+ @call.stubs(:agents_available?).returns(true)
121
+ @call.incoming_call!
122
+
123
+ body @call.render
124
+ assert_select "Response"
125
+ end
126
+
127
+ should "respond when flow to state (once)" do
128
+ @call.state = 'routing'
129
+ @call.expects(:notify).with(:cancelled).once
130
+ @call.customer_hangs_up!
131
+ assert @call.cancelled?
132
+ @call.customer_hangs_up!
133
+ assert @call.cancelled?
134
+ @call.customer_hangs_up!
135
+ assert @call.cancelled?
136
+ end
137
+
138
+ should "asynchronously perform event" do
139
+ @call.stubs(:agents_available?).returns(true)
140
+ @call.incoming_call!
141
+ @call.expects(:redirect_to).with(:start_conference)
142
+
143
+ @call.redirect_and_start_conference!
144
+ end
145
+
146
+ should "asynchronously perform event with options" do
147
+ @call.stubs(:agents_available?).returns(true)
148
+ @call.incoming_call!
149
+ @call.expects(:redirect_to).with(:start_conference, :status => 'completed')
150
+
151
+ @call.redirect_and_start_conference!(:status => 'completed')
152
+ end
153
+
154
+ should "raise error on missing method" do
155
+ assert_raises {
156
+ @call.i_am_missing!
157
+ }
158
+ end
159
+
160
+ should "draw state machine digraph" do
161
+ Call.state_machines[:state].expects(:draw).with(:name => 'call_workflow', :font => 'Helvetica Neue')
162
+ @call.draw_call_flow(:name => 'call_workflow', :font => 'Helvetica Neue')
163
+ end
164
+
165
+ context "using test DSL:" do
166
+ should_flow :on => :incoming_call, :initial => :voicemail, :when => Proc.new {
167
+ @call.stubs(:agents_available?).returns(false)
168
+ @call.stubs(:notify)
169
+ @call.stubs(:flow_url).returns('the_flow')
170
+ } do
171
+ should_also { assert_received(@call, :notify) { |e| e.with(:rendering_voicemail) } }
172
+ and_also { assert_received(@call, :flow_url) { |e| e.with(:voicemail_complete) } }
173
+ and_render { "Response>Say" }
174
+ and_render { "Response>Record[action=the_flow]" }
175
+ end
176
+
177
+ should_flow :on => :incoming_call, :initial => :routing, :when => Proc.new {
178
+ @call.stubs(:agents_available?).returns(true)
179
+ } do
180
+ should_render { "Response" }
181
+ end
182
+
183
+ should_flow :on => :customer_hangs_up, :routing => :cancelled, :when => Proc.new {
184
+ @call.stubs(:notify)
185
+ } do
186
+ should_also { assert_received(@call, :notify) { |e| e.with(:cancelled).once } }
187
+ and_also { assert @call.cancelled? }
188
+
189
+ should_flow :on => :customer_hangs_up, :cancelled => :cancelled do
190
+ should_also { assert_received(@call, :notify) { |e| e.with(:cancelled).once } } # For above
191
+ and_also { assert @call.cancelled? }
192
+
193
+ should_flow :on => :customer_hangs_up, :cancelled => :cancelled do
194
+ should_also { assert_received(@call, :notify) { |e| e.with(:cancelled).once } } # For above
195
+ and_also { assert @call.cancelled? }
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
201
+
202
+ context "non-standard call" do
203
+ setup do
204
+ @call = NonStandardCall.new
205
+ end
206
+
207
+ should "render xml for initial state" do
208
+ assert_equal 'ready', @call.status
209
+ body @call.render
210
+ assert_select "Response>Say", "Hello World"
211
+ end
212
+ end
213
+
214
+ context "cache call" do
215
+ should "re-apply state machine and render xml for initial state" do
216
+ Object.send(:remove_const, :NonStandardCall)
217
+ Object.const_set(:NonStandardCall, Class.new)
218
+ NonStandardCall.class_eval do
219
+ include CallCenter
220
+ call_flow :status, :initial => :ready do
221
+ raise Exception, "Should not be called"
222
+ end
223
+ end
224
+
225
+ @call = NonStandardCall.new
226
+
227
+ assert_equal 'ready', @call.status
228
+ body @call.render
229
+ assert_select "Response>Say", "Hello World"
230
+ assert @call.go!
231
+ end
232
+ end
233
+
234
+ context "cache multiple call flows" do
235
+ should "re-apply state machine and render xml for initial state" do
236
+ Object.send(:remove_const, :MultipleFlowCall)
237
+ Object.const_set(:MultipleFlowCall, Class.new)
238
+ MultipleFlowCall.class_eval do
239
+ include CallCenter
240
+ call_flow :status, :initial => :ready do
241
+ raise Exception, "Should not be called"
242
+ end
243
+ call_flow :outgoing_status, :initial => :outgoing_ready do
244
+ raise Exception, "Should not be called"
245
+ end
246
+ end
247
+
248
+ @call = MultipleFlowCall.new
249
+
250
+ assert_equal 'ready', @call.status
251
+ body @call.render
252
+ assert_select "Response>Say", "Hello World"
253
+ assert @call.go!
254
+
255
+ assert_equal 'outgoing_ready', @call.outgoing_status
256
+ body @call.render(:outgoing_status)
257
+ assert_select "Response>Say", "Hello Outgoing World"
258
+ assert @call.outgoing_go!
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,18 @@
1
+ require 'helper'
2
+
3
+ class MyObject
4
+
5
+ end
6
+
7
+ class CoreExtTest < Test::Unit::TestCase
8
+ should "use existing" do
9
+ obj = MyObject.new
10
+ $capture = nil
11
+ block = lambda { |a|
12
+ $capture = [self, a]
13
+ }
14
+ obj.instance_exec(true, &block)
15
+
16
+ assert_equal [obj, true], $capture
17
+ end
18
+ end
@@ -0,0 +1,46 @@
1
+ class Call
2
+ include CallCenter
3
+ include CommonCallMethods
4
+
5
+ call_flow :state, :initial => :initial do
6
+ state :initial do
7
+ event :incoming_call, :to => :voicemail, :unless => :agents_available?
8
+ event :incoming_call, :to => :routing, :if => :agents_available?
9
+ event :something_crazy_happens, :to => :uh_oh
10
+ end
11
+
12
+ state :voicemail do
13
+ event :customer_hangs_up, :to => :voicemail_completed
14
+ end
15
+
16
+ state :routing do
17
+ event :customer_hangs_up, :to => :cancelled
18
+ event :start_conference, :to => :in_conference
19
+ end
20
+
21
+ state :cancelled do
22
+ event :customer_hangs_up, :to => same
23
+ end
24
+
25
+ # =================
26
+ # = Render Blocks =
27
+ # =================
28
+
29
+ state :initial do
30
+ on_render do |call, x| # To allow defining render blocks within a state
31
+ call.notify(:rendering_initial)
32
+ x.Say "Hello World"
33
+ end
34
+ end
35
+
36
+ on_render(:voicemail) do |call, x| # To allow defining render blocks outside a state
37
+ notify(:rendering_voicemail)
38
+ x.Say "Hello World"
39
+ x.Record :action => flow_url(:voicemail_complete)
40
+ end
41
+
42
+ on_flow_to(:cancelled) do |call, transition|
43
+ notify(:cancelled)
44
+ end
45
+ end
46
+ end