call_center 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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