Added browser tests for customer chat.

This commit is contained in:
Martin Edenhofer 2015-12-05 20:41:14 +01:00
parent 0cb6e34870
commit 7831f1c1f8
18 changed files with 1418 additions and 566 deletions

View file

@ -24,7 +24,7 @@ class App.ChannelChat extends App.Controller
'.js-code': 'code' '.js-code': 'code'
'.js-palette': 'palette' '.js-palette': 'palette'
'.js-color': 'colorField' '.js-color': 'colorField'
'.js-chatSetting': 'chatSetting' '.js-chatSetting input': 'chatSetting'
apiOptions: [ apiOptions: [
{ {
@ -279,6 +279,8 @@ class App.ChannelChat extends App.Controller
setting.state_current = { value: value } setting.state_current = { value: value }
setting.save() setting.save()
@Config.set('chat', value) @Config.set('chat', value)
delay = -> App.Event.trigger('ui:rerender')
@delay(delay, 200)
updateParams: => updateParams: =>
quote = (value) -> quote = (value) ->

View file

@ -7,7 +7,7 @@ class App.ChannelForm extends App.Controller
elements: elements:
'.js-paramsBlock': 'paramsBlock' '.js-paramsBlock': 'paramsBlock'
'.js-formSetting': 'formSetting' '.js-formSetting input': 'formSetting'
constructor: -> constructor: ->
super super

View file

@ -11,14 +11,6 @@ class App.CustomerChat extends App.Controller
constructor: -> constructor: ->
super super
# access check
if !@isRole('Chat')
@renderScreenUnauthorized(objectName: 'Chat')
return
if !@Config.get('chat')
@renderScreenError(detail: 'Feature disabled!')
return
@chatWindows = {} @chatWindows = {}
@maxChatWindows = 4 @maxChatWindows = 4
preferences = @Session.get('preferences') preferences = @Session.get('preferences')
@ -49,7 +41,6 @@ class App.CustomerChat extends App.Controller
# add new chat window # add new chat window
@bind('chat_session_start', (data) => @bind('chat_session_start', (data) =>
console.log('chat_session_start', data)
if data.session if data.session
@addChat(data.session) @addChat(data.session)
) )
@ -78,6 +69,13 @@ class App.CustomerChat extends App.Controller
) )
render: -> render: ->
if !@isRole('Chat')
@renderScreenUnauthorized(objectName: 'Chat')
return
if !@Config.get('chat')
@renderScreenError(detail: 'Feature disabled!')
return
@html App.view('customer_chat/index')() @html App.view('customer_chat/index')()
show: (params) => show: (params) =>

View file

@ -82,7 +82,7 @@ class App.Navigation extends App.ControllerWidgetPermanent
) )
# bind on switch changes and execute it on controller # bind on switch changes and execute it on controller
@$('.js-menu .js-switch').bind('change', (e) -> @$('.js-menu .js-switch input').bind('change', (e) ->
val = $(e.target).prop('checked') val = $(e.target).prop('checked')
key = $(e.target).closest('.menu-item').data('key') key = $(e.target).closest('.menu-item').data('key')
return if !key return if !key

View file

@ -8,8 +8,8 @@
<h2><%- @T('Enable') %>/<%- @T('Disable') %></h2> <h2><%- @T('Enable') %>/<%- @T('Disable') %></h2>
<form> <form>
<div class="zammad-switch"> <div class="zammad-switch js-chatSetting">
<input name="chat" type="checkbox" id="setting-chat" class="js-chatSetting" <% if @chatSetting: %>checked<% end %>> <input name="chat" type="checkbox" id="setting-chat" <% if @chatSetting: %>checked<% end %>>
<label for="setting-chat"></label> <label for="setting-chat"></label>
</div> </div>
</form> </form>

View file

@ -8,8 +8,8 @@
<h2><%- @T('Enable') %>/<%- @T('Disable') %></h2> <h2><%- @T('Enable') %>/<%- @T('Disable') %></h2>
<form> <form>
<div class="zammad-switch"> <div class="zammad-switch js-formSetting">
<input name="form_ticket_create" type="checkbox" id="setting-form" class="js-formSetting" <% if @formSetting: %>checked<% end %>> <input name="form_ticket_create" type="checkbox" id="setting-form" <% if @formSetting: %>checked<% end %>>
<label for="setting-form"></label> <label for="setting-form"></label>
</div> </div>
</form> </form>

View file

@ -30,8 +30,8 @@
<span class="counter badge badge--big"><%= item.counter %></span> <span class="counter badge badge--big"><%= item.counter %></span>
<% end %> <% end %>
<% if item.switch isnt undefined: %> <% if item.switch isnt undefined: %>
<span class="zammad-switch zammad-switch--dark zammad-switch--small zammad-switch--green"> <span class="zammad-switch zammad-switch--dark zammad-switch--small zammad-switch--green js-switch">
<input type="checkbox" id="<%- item.class %>-switch" class="js-switch" <% if item.switch: %>checked<% end %>> <input type="checkbox" id="<%- item.class %>-switch" <% if item.switch: %>checked<% end %>>
<label for="<%- item.class %>-switch"></label> <label for="<%- item.class %>-switch"></label>
</span> </span>
<% end %> <% end %>

View file

@ -18,6 +18,18 @@ class Chat::Session < ApplicationModel
preferences[:participants] preferences[:participants]
end end
def recipients_active?
return true if !preferences
return true if !preferences[:participants]
count = 0
preferences[:participants].each {|client_id|
next if !Sessions.session_exists?(client_id)
count += 1
}
return true if count >= 2
false
end
def send_to_recipients(message, ignore_client_id = nil) def send_to_recipients(message, ignore_client_id = nil)
preferences[:participants].each {|local_client_id| preferences[:participants].each {|local_client_id|
next if local_client_id == ignore_client_id next if local_client_id == ignore_client_id

View file

@ -0,0 +1,36 @@
# encoding: utf-8
class Observer::Chat::Leave::BackgroundJob
def initialize(chat_session_id, client_id, session)
@chat_session_id = chat_session_id
@client_id = client_id
@session = session
end
def perform
# check if customer has permanently left the conversation
chat_session = Chat::Session.find_by(id: @chat_session_id)
return if !chat_session
return if chat_session.recipients_active?
chat_session.state = 'closed'
chat_session.save
realname = 'Anonymous'
if @session && @session['id']
realname = User.lookup(id: @session['id']).fullname
end
# notifiy participients
message = {
event: 'chat_session_left',
data: {
realname: realname,
session_id: chat_session.session_id,
},
}
chat_session.send_to_recipients(message, @client_id)
end
end

View file

@ -16,7 +16,11 @@ class Setting < ApplicationModel
@@current = {} # rubocop:disable Style/ClassVars @@current = {} # rubocop:disable Style/ClassVars
@@change_id = nil # rubocop:disable Style/ClassVars @@change_id = nil # rubocop:disable Style/ClassVars
@@lookup_at = nil # rubocop:disable Style/ClassVars @@lookup_at = nil # rubocop:disable Style/ClassVars
@@lookup_timeout = 2.minutes # rubocop:disable Style/ClassVars if ENV['ZAMMAD_SETTING_TTL']
@@lookup_timeout = ENV['ZAMMAD_SETTING_TTL'].to_i # rubocop:disable Style/ClassVars
else
@@lookup_timeout = 2.minutes # rubocop:disable Style/ClassVars
end
=begin =begin

View file

@ -5,8 +5,135 @@ do($ = window.jQuery, window) ->
scriptHost = myScript.src.match('.*://([^:/]*).*')[1] scriptHost = myScript.src.match('.*://([^:/]*).*')[1]
# Define the plugin class # Define the plugin class
class ZammadChat class Base
defaults:
debug: false
constructor: (options) ->
@options = $.extend {}, @defaults, options
@log = new Log(debug: @options.debug, logPrefix: @options.logPrefix || @logPrefix)
class Log
defaults:
debug: false
constructor: (options) ->
@options = $.extend {}, @defaults, options
debug: (items...) =>
return if !@options.debug && level is 'debug'
@log('debug', items)
notice: (items...) =>
@log('notice', items)
error: (items...) =>
@log('error', items)
return if !@options.debug && level is 'debug'
items.unshift(level)
console.log.apply console, string
log: (level, items) =>
items.unshift('||')
items.unshift(level)
items.unshift(@options.logPrefix)
console.log.apply console, items
return if !@options.debug
logString = ''
for item in items
logString += ' '
if typeof item is 'object'
logString += JSON.stringify(item)
else if item && item.toString
logString += item.toString()
else
logString += item
$('.js-chatLogDisplay').prepend('<div>' + logString + '</div>')
class Timeout extends Base
timeoutStartedAt: null
logPrefix: 'timeout'
defaults:
debug: false
timeout: 4
timeoutIntervallCheck: 0.5
constructor: (options) ->
super(options)
start: =>
@stop()
timeoutStartedAt = new Date
check = =>
timeLeft = new Date - new Date(timeoutStartedAt.getTime() + @options.timeout * 1000 * 60)
@log.debug "Timeout check for #{@options.timeout} minutes (left #{timeLeft/1000} sec.)"#, new Date
return if timeLeft < 0
@stop()
@options.callback()
@log.debug "Start timeout in #{@options.timeout} minutes"#, new Date
@intervallId = setInterval(check, @options.timeoutIntervallCheck * 1000 * 60)
stop: =>
return if !@intervallId
@log.debug "Stop timeout of #{@options.timeout} minutes at"#, new Date
clearInterval(@intervallId)
class Io extends Base
logPrefix: 'io'
constructor: (options) ->
super(options)
set: (params) =>
for key, value of params
@options[key] = value
detectHost: ->
protocol = 'ws://'
if window.location.protocol is 'https:'
protocol = 'wss://'
@options.host = "#{ protocol }#{ scriptHost }/ws"
connect: =>
@detectHost() if !@options.host
@log.debug "Connecting to #{@options.host}"
@ws = new window.WebSocket("#{@options.host}")
@ws.onopen = (e) =>
@log.debug 'on open', e
@options.onOpen(e)
@ws.onmessage = (e) =>
pipes = JSON.parse(e.data)
@log.debug 'on message', e.data
if @options.onMessage
@options.onMessage(pipes)
@ws.onclose = (e) =>
@log.debug 'close websocket connection'
if @options.onClose
@options.onClose(e)
@ws.onerror = (e) =>
@log.debug 'onerror', e
if @options.onError
@options.onError(e)
close: =>
@ws.close()
reconnect: =>
@ws.close()
@connect()
send: (event, data = {}) =>
@log.debug 'send', event, data
msg = JSON.stringify
event: event
data: data
@ws.send msg
class ZammadChat extends Base
defaults: defaults:
chatId: undefined chatId: undefined
show: true show: true
@ -21,9 +148,14 @@ do($ = window.jQuery, window) ->
buttonClass: 'open-zammad-chat' buttonClass: 'open-zammad-chat'
inactiveClass: 'is-inactive' inactiveClass: 'is-inactive'
title: '<strong>Chat</strong> with us!' title: '<strong>Chat</strong> with us!'
idleTimeout: 4 idleTimeout: 8
inactiveTimeout: 20 idleTimeoutIntervallCheck: 0.5
inactiveTimeout: 8
inactiveTimeoutIntervallCheck: 0.5
waitingListTimeout: 4
waitingListTimeoutIntervallCheck: 0.5
logPrefix: 'chat'
_messageCount: 0 _messageCount: 0
isOpen: true isOpen: true
blinkOnlineInterval: null blinkOnlineInterval: null
@ -35,7 +167,6 @@ do($ = window.jQuery, window) ->
isTyping: false isTyping: false
state: 'offline' state: 'offline'
initialQueueDelay: 10000 initialQueueDelay: 10000
wsReconnectEnable: true
translations: translations:
de: de:
'<strong>Chat</strong> with us!': '<strong>Chat</strong> mit uns!' '<strong>Chat</strong> with us!': '<strong>Chat</strong> mit uns!'
@ -57,22 +188,17 @@ do($ = window.jQuery, window) ->
T: (string, items...) => T: (string, items...) =>
if @options.lang && @options.lang isnt 'en' if @options.lang && @options.lang isnt 'en'
if !@translations[@options.lang] if !@translations[@options.lang]
@log 'notice', "Translation '#{@options.lang}' needed!" @log.notice "Translation '#{@options.lang}' needed!"
else else
translations = @translations[@options.lang] translations = @translations[@options.lang]
if !translations[string] if !translations[string]
@log 'notice', "Translation needed for '#{string}'" @log.notice "Translation needed for '#{string}'"
string = translations[string] || string string = translations[string] || string
if items if items
for item in items for item in items
string = string.replace(/%s/, item) string = string.replace(/%s/, item)
string string
log: (level, string...) =>
return if !@options.debug && level is 'debug'
string.unshift(level)
console.log.apply console, string
view: (name) => view: (name) =>
return (options) => return (options) =>
if !options if !options
@ -86,19 +212,20 @@ do($ = window.jQuery, window) ->
constructor: (options) -> constructor: (options) ->
@options = $.extend {}, @defaults, options @options = $.extend {}, @defaults, options
super(@options)
# check prerequisites # check prerequisites
if !$ if !$
@state = 'unsupported' @state = 'unsupported'
@log 'notice', 'Chat: no jquery found!' @log.notice 'Chat: no jquery found!'
return return
if !window.WebSocket or !sessionStorage if !window.WebSocket or !sessionStorage
@state = 'unsupported' @state = 'unsupported'
@log 'notice', 'Chat: Browser not supported!' @log.notice 'Chat: Browser not supported!'
return return
if !@options.chatId if !@options.chatId
@state = 'unsupported' @state = 'unsupported'
@log 'error', 'Chat: need chatId as option!' @log.error 'Chat: need chatId as option!'
return return
# detect language # detect language
@ -106,8 +233,19 @@ do($ = window.jQuery, window) ->
@options.lang = $('html').attr('lang') @options.lang = $('html').attr('lang')
if @options.lang if @options.lang
@options.lang = @options.lang.replace(/-.+?$/, '') # replace "-xx" of xx-xx @options.lang = @options.lang.replace(/-.+?$/, '') # replace "-xx" of xx-xx
@log 'debug', "lang: #{@options.lang}" @log.debug "lang: #{@options.lang}"
@loadCss()
@io = new Io(@options)
@io.set(
onOpen: @render
onClose: @hide
onMessage: @onWebSocketMessage
)
@io.connect()
render: =>
@el = $(@view('chat')( @el = $(@view('chat')(
title: @options.title title: @options.title
)) ))
@ -124,10 +262,20 @@ do($ = window.jQuery, window) ->
@input.on @input.on
keydown: @checkForEnter keydown: @checkForEnter
input: @onInput input: @onInput
$(window).on('beforeunload', =>
@onLeaveTemporary()
)
@setAgentOnlineState 'online'
@wsConnect() @log.debug 'widget rendered'
@loadCss() @startTimeoutObservers()
@idleTimeout.start()
# get current chat status
@sessionId = sessionStorage.getItem('sessionId')
@send 'chat_status_customer',
session_id: @sessionId
checkForEnter: (event) => checkForEnter: (event) =>
if not event.shiftKey and event.keyCode is 13 if not event.shiftKey and event.keyCode is 13
@ -136,22 +284,16 @@ do($ = window.jQuery, window) ->
send: (event, data = {}) => send: (event, data = {}) =>
data.chat_id = @options.chatId data.chat_id = @options.chatId
@log 'debug', 'ws:send', event, data @io.send(event, data)
pipe = JSON.stringify
event: event
data: data
@ws.send pipe
onWebSocketMessage: (e) =>
pipes = JSON.parse( e.data )
onWebSocketMessage: (pipes) =>
for pipe in pipes for pipe in pipes
@log 'debug', 'ws:onmessage', pipe @log.debug 'ws:onmessage', pipe
switch pipe.event switch pipe.event
when 'chat_error' when 'chat_error'
@log 'notice', pipe.data @log.notice pipe.data
if pipe.data && pipe.data.state is 'chat_disabled' if pipe.data && pipe.data.state is 'chat_disabled'
@wsClose() @destroy(hide: true)
when 'chat_session_message' when 'chat_session_message'
return if pipe.data.self_written return if pipe.data.self_written
@receiveMessage pipe.data @receiveMessage pipe.data
@ -171,38 +313,35 @@ do($ = window.jQuery, window) ->
when 'online' when 'online'
@sessionId = undefined @sessionId = undefined
@onReady() @onReady()
@log 'debug', 'Zammad Chat: ready'
when 'offline' when 'offline'
@onError 'Zammad Chat: No agent online' @onError 'Zammad Chat: No agent online'
@state = 'off' @state = 'off'
@hide() @destroy(hide: true)
@wsClose()
when 'chat_disabled' when 'chat_disabled'
@onError 'Zammad Chat: Chat is disabled' @onError 'Zammad Chat: Chat is disabled'
@state = 'off' @state = 'off'
@hide() @destroy(hide: true)
@wsClose()
when 'no_seats_available' when 'no_seats_available'
@onError "Zammad Chat: Too many clients in queue. Clients in queue: #{pipe.data.queue}" @onError "Zammad Chat: Too many clients in queue. Clients in queue: #{pipe.data.queue}"
@state = 'off' @state = 'off'
@hide() @destroy(hide: true)
@wsClose()
when 'reconnect' when 'reconnect'
@log 'debug', 'old messages', pipe.data.session @log.debug 'old messages', pipe.data.session
@reopenSession pipe.data @reopenSession pipe.data
onReady: => onReady: =>
@log.debug 'widget ready for use'
$(".#{ @options.buttonClass }").click(@open).removeClass(@inactiveClass) $(".#{ @options.buttonClass }").click(@open).removeClass(@inactiveClass)
if @options.show if @options.show
@show() @show()
onError: (message) => onError: (message) =>
@log 'debug', message @log.debug message
$(".#{ @options.buttonClass }").hide() $(".#{ @options.buttonClass }").hide()
reopenSession: (data) => reopenSession: (data) =>
@inactiveTimeoutStart() @inactiveTimeout.start()
unfinishedMessage = sessionStorage.getItem 'unfinished_message' unfinishedMessage = sessionStorage.getItem 'unfinished_message'
@ -246,7 +385,7 @@ do($ = window.jQuery, window) ->
@isTyping = new Date() @isTyping = new Date()
@send 'chat_session_typing', @send 'chat_session_typing',
session_id: @sessionId session_id: @sessionId
@inactiveTimeoutStart() @inactiveTimeout.start()
onSubmit: (event) => onSubmit: (event) =>
event.preventDefault() event.preventDefault()
@ -256,7 +395,7 @@ do($ = window.jQuery, window) ->
message = @input.val() message = @input.val()
return if !message return if !message
@inactiveTimeoutStart() @inactiveTimeout.start()
sessionStorage.removeItem 'unfinished_message' sessionStorage.removeItem 'unfinished_message'
@ -286,7 +425,7 @@ do($ = window.jQuery, window) ->
session_id: @sessionId session_id: @sessionId
receiveMessage: (data) => receiveMessage: (data) =>
@inactiveTimeoutStart() @inactiveTimeout.start()
# hide writing indicator # hide writing indicator
@onAgentTypingEnd() @onAgentTypingEnd()
@ -305,6 +444,7 @@ do($ = window.jQuery, window) ->
@scrollToBottom() @scrollToBottom()
open: => open: =>
@log.debug 'open widget'
if @isOpen if @isOpen
@show() @show()
@ -322,12 +462,14 @@ do($ = window.jQuery, window) ->
@isOpen = true @isOpen = true
if !@sessionId if !@sessionId
@sessionInit() @send('chat_session_init')
onOpenAnimationEnd: => onOpenAnimationEnd: =>
@idleTimeoutStop() @idleTimeout.stop()
close: (event) => close: (event) =>
@log.debug 'close widget'
return @state if @state is 'off' or @state is 'unsupported' return @state if @state is 'off' or @state is 'unsupported'
event.stopPropagation() if event event.stopPropagation() if event
@ -339,7 +481,8 @@ do($ = window.jQuery, window) ->
session_id: @sessionId session_id: @sessionId
# stop timer # stop timer
@inactiveTimeoutStop() @inactiveTimeout.stop()
@waitingListTimeout.stop()
# delete input store # delete input store
sessionStorage.removeItem 'unfinished_message' sessionStorage.removeItem 'unfinished_message'
@ -360,14 +503,20 @@ do($ = window.jQuery, window) ->
onCloseAnimationEnd: => onCloseAnimationEnd: =>
@el.removeClass('zammad-chat-is-visible') @el.removeClass('zammad-chat-is-visible')
@disconnect()
@showLoader()
@el.find('.zammad-chat-welcome').removeClass('zammad-chat-is-hidden')
@el.find('.zammad-chat-agent').addClass('zammad-chat-is-hidden')
@el.find('.zammad-chat-agent-status').addClass('zammad-chat-is-hidden')
@isOpen = false @isOpen = false
# restart connection # restart connection
@onWebSocketOpen() @io.reconnect()
hide: -> hide: ->
@el.removeClass('zammad-chat-is-shown') if @el
@el.removeClass('zammad-chat-is-shown')
show: -> show: ->
return @state if @state is 'off' or @state is 'unsupported' return @state if @state is 'off' or @state is 'unsupported'
@ -397,6 +546,9 @@ do($ = window.jQuery, window) ->
# delay initial queue position, show connecting first # delay initial queue position, show connecting first
show = => show = =>
@onQueue data @onQueue data
console.log('onQueueScreen')
@waitingListTimeout.start()
if @initialQueueDelay && !@onInitialQueueDelayId if @initialQueueDelay && !@onInitialQueueDelayId
@onInitialQueueDelayId = setTimeout(show, @initialQueueDelay) @onInitialQueueDelayId = setTimeout(show, @initialQueueDelay)
return return
@ -409,7 +561,7 @@ do($ = window.jQuery, window) ->
show() show()
onQueue: (data) => onQueue: (data) =>
@log 'notice', 'onQueue', data.position @log.notice 'onQueue', data.position
@inQueue = true @inQueue = true
@el.find('.zammad-chat-body').html @view('waiting') @el.find('.zammad-chat-body').html @view('waiting')
@ -432,6 +584,11 @@ do($ = window.jQuery, window) ->
onAgentTypingEnd: => onAgentTypingEnd: =>
@el.find('.zammad-chat-message--typing').remove() @el.find('.zammad-chat-message--typing').remove()
onLeaveTemporary: =>
return if !@sessionId
@send 'chat_session_leave_temporary',
session_id: @sessionId
maybeAddTimestamp: -> maybeAddTimestamp: ->
timestamp = Date.now() timestamp = Date.now()
@ -470,59 +627,38 @@ do($ = window.jQuery, window) ->
scrollToBottom: -> scrollToBottom: ->
@el.find('.zammad-chat-body').scrollTop($('.zammad-chat-body').prop('scrollHeight')) @el.find('.zammad-chat-body').scrollTop($('.zammad-chat-body').prop('scrollHeight'))
sessionInit: ->
@send('chat_session_init')
detectHost: -> detectHost: ->
protocol = 'ws://' protocol = 'ws://'
if window.location.protocol is 'https:' if window.location.protocol is 'https:'
protocol = 'wss://' protocol = 'wss://'
@options.host = "#{ protocol }#{ scriptHost }/ws" @options.host = "#{ protocol }#{ scriptHost }/ws"
wsConnect: => destroy: (params = {}) =>
@detectHost() if !@options.host @log.debug 'destroy widget'
console.log('el', @el)
if params.hide
if @el
@el.remove()
@wsReconnectStop()
@io.close()
@log 'debug', "Connecting to #{@options.host}" wsReconnectStart: =>
@ws = new window.WebSocket("#{@options.host}") @wsReconnectStop()
@ws.onopen = @onWebSocketOpen
@ws.onmessage = @onWebSocketMessage
@ws.onclose = (e) =>
@log 'debug', 'close websocket connection'
if @wsReconnectEnable
@reconnect()
@ws.onerror = (e) =>
@log 'debug', 'ws:onerror', e
wsClose: =>
@wsReconnectEnable = false
@ws.close()
wsReconnect: =>
if @reconnectDelayId if @reconnectDelayId
clearTimeout(@reconnectDelayId) clearTimeout(@reconnectDelayId)
@reconnectDelayId = setTimeout(@wsConnect, 5000) @reconnectDelayId = setTimeout(@io.connect(), 5000)
onWebSocketOpen: => wsReconnectStop: =>
@idleTimeoutStart() if @reconnectDelayId
@sessionId = sessionStorage.getItem('sessionId') clearTimeout(@reconnectDelayId)
@log 'debug', 'ws connected'
@send 'chat_status_customer',
session_id: @sessionId
@setAgentOnlineState 'online'
reconnect: => reconnect: =>
# set status to connecting # set status to connecting
@log 'notice', 'reconnecting' @log.notice 'reconnecting'
@disableInput() @disableInput()
@lastAddedType = 'status' @lastAddedType = 'status'
@setAgentOnlineState 'connecting' @setAgentOnlineState 'connecting'
@addStatus @T('Connection lost') @addStatus @T('Connection lost')
@wsReconnect()
onConnectionReestablished: => onConnectionReestablished: =>
# set status back to online # set status back to online
@ -534,13 +670,7 @@ do($ = window.jQuery, window) ->
@addStatus @T('Chat closed by %s', data.realname) @addStatus @T('Chat closed by %s', data.realname)
@disableInput() @disableInput()
@setAgentOnlineState 'offline' @setAgentOnlineState 'offline'
@inactiveTimeoutStop() @inactiveTimeout.stop()
disconnect: ->
@showLoader()
@el.find('.zammad-chat-welcome').removeClass('zammad-chat-is-hidden')
@el.find('.zammad-chat-agent').addClass('zammad-chat-is-hidden')
@el.find('.zammad-chat-agent-status').addClass('zammad-chat-is-hidden')
setSessionId: (id) => setSessionId: (id) =>
@sessionId = id @sessionId = id
@ -573,8 +703,12 @@ do($ = window.jQuery, window) ->
@setAgentOnlineState 'online' @setAgentOnlineState 'online'
showTimeout: -> @waitingListTimeout.stop()
@el.find('.zammad-chat-body').html @view('timeout') @idleTimeout.stop()
@inactiveTimeout.start()
showCustomerTimeout: ->
@el.find('.zammad-chat-body').html @view('customer_timeout')
agent: @agent.name agent: @agent.name
delay: @options.inactiveTimeout delay: @options.inactiveTimeout
@close() @close()
@ -582,6 +716,14 @@ do($ = window.jQuery, window) ->
location.reload() location.reload()
@el.find('.js-restart').click reload @el.find('.js-restart').click reload
showWaitingListTimeout: ->
@el.find('.zammad-chat-body').html @view('waiting_list_timeout')
delay: @options.watingListTimeout
@close()
reload = ->
location.reload()
@el.find('.js-restart').click reload
showLoader: -> showLoader: ->
@el.find('.zammad-chat-body').html @view('loader')() @el.find('.zammad-chat-body').html @view('loader')()
@ -603,42 +745,47 @@ do($ = window.jQuery, window) ->
.replace(/\/ws/i, '') .replace(/\/ws/i, '')
url += '/assets/chat/chat.css' url += '/assets/chat/chat.css'
@log 'debug', "load css from '#{url}'" @log.debug "load css from '#{url}'"
styles = "@import url('#{url}');" styles = "@import url('#{url}');"
newSS = document.createElement('link') newSS = document.createElement('link')
newSS.rel = 'stylesheet' newSS.rel = 'stylesheet'
newSS.href = 'data:text/css,' + escape(styles) newSS.href = 'data:text/css,' + escape(styles)
document.getElementsByTagName('head')[0].appendChild(newSS) document.getElementsByTagName('head')[0].appendChild(newSS)
inactiveTimeoutStart: => startTimeoutObservers: =>
@inactiveTimeoutStop() @idleTimeout = new Timeout(
delay = => logPrefix: 'idleTimeout'
@log 'debug', "Inactive timeout of #{@options.inactiveTimeout} minutes, show timeout screen.", new Date debug: @options.debug
@state = 'off' timeout: @options.idleTimeout
@setAgentOnlineState 'offline' timeoutIntervallCheck: @options.idleTimeoutIntervallCheck
@showTimeout() callback: =>
@wsClose() @log.debug 'Idle timeout reached, hide widget', new Date
@log 'debug', "Start inactive timeout in #{@options.inactiveTimeout} minutes", new Date @state = 'off'
@inactiveTimeoutStopDelayId = setTimeout(delay, @options.inactiveTimeout * 1000 * 60) @destroy(hide: true)
)
inactiveTimeoutStop: => @inactiveTimeout = new Timeout(
return if !@inactiveTimeoutStopDelayId logPrefix: 'inactiveTimeout'
@log 'debug', "Stop inactive timeout of #{@options.inactiveTimeout} minutes at", new Date debug: @options.debug
clearTimeout(@inactiveTimeoutStopDelayId) timeout: @options.inactiveTimeout
timeoutIntervallCheck: @options.inactiveTimeoutIntervallCheck
idleTimeoutStart: => callback: =>
@idleTimeoutStop() @log.debug 'Inactive timeout reached, show timeout screen.', new Date
delay = => @state = 'off'
@log 'debug', "Idle timeout of #{@options.idleTimeout} minutes, hide widget", new Date @setAgentOnlineState 'offline'
@state = 'off' @showCustomerTimeout()
@hide() @destroy(hide:false)
@wsClose() )
@log 'debug', "Start idle timeout in #{@options.idleTimeout} minutes", new Date @waitingListTimeout = new Timeout(
@idleTimeoutStopDelayId = setTimeout(delay, @options.idleTimeout * 1000 * 60) logPrefix: 'waitingListTimeout'
debug: @options.debug
idleTimeoutStop: => timeout: @options.waitingListTimeout
return if !@idleTimeoutStopDelayId timeoutIntervallCheck: @options.waitingListTimeoutIntervallCheck
@log 'debug', "Stop idle timeout of #{@options.idleTimeout} minutes at", new Date callback: =>
clearTimeout(@idleTimeoutStopDelayId) @log.debug 'Waiting list timeout reached, show timeout screen.', new Date
@state = 'off'
@setAgentOnlineState 'offline'
@showWaitingListTimeout()
@destroy(hide:false)
)
window.ZammadChat = ZammadChat window.ZammadChat = ZammadChat

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,7 @@
<div class="zammad-chat-modal">
<div class="zammad-chat-modal-text">
<%- @T('We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!') %>
<br>
<div class="zammad-chat-button js-restart"<%= " style='background: #{ @background }'" if @background %>><%- @T('Start new conversation') %></div>
</div>
</div>

View file

@ -25,6 +25,7 @@
padding: 10px; padding: 10px;
border-radius: 5px; border-radius: 5px;
box-shadow: 0 3px 10px rgba(0,0,0,.3); box-shadow: 0 3px 10px rgba(0,0,0,.3);
width: 500px;
} }
.settings input { .settings input {
@ -39,6 +40,12 @@
padding-right: 0; padding-right: 0;
} }
table td.log {
text-align: left;
padding-right: 0;
word-break: break-all;
}
td { td {
padding: 5px; padding: 5px;
} }
@ -73,8 +80,8 @@
<div class="settings"> <div class="settings">
<table> <table>
<tr> <tr>
<td>
<td><h2>Settings</h2> <td><h2>Settings</h2>
<td>
<tr> <tr>
<td> <td>
<input id="flat" type="checkbox" data-option="flat"> <input id="flat" type="checkbox" data-option="flat">
@ -100,6 +107,11 @@
<tr> <tr>
<td> <td>
<td><button class="open-zammad-chat">Open Chat</button> <td><button class="open-zammad-chat">Open Chat</button>
<tr>
<td class="log"><h2>Log</h2>
<td>
<tr>
<td colspan="2" class="log js-chatLogDisplay">
</table> </table>
</div> </div>
@ -113,7 +125,13 @@
debug: true, debug: true,
background: '#494d52', background: '#494d52',
flat: true, flat: true,
shown: false shown: false,
idleTimeout: 1,
idleTimeoutIntervallCheck: 0.5,
inactiveTimeout: 2,
inactiveTimeoutIntervallCheck: 0.5,
waitingListTimeout: 1.2,
waitingListTimeoutIntervallCheck: 0.5,
}); });
$('.settings :input').on({ $('.settings :input').on({

View file

@ -257,6 +257,7 @@ EventMachine.run {
} }
elsif data['event'] elsif data['event']
log 'notice', "execute event '#{data['event']}'", client_id
message = Sessions::Event.run(data['event'], data, @clients[client_id][:session], client_id) message = Sessions::Event.run(data['event'], data, @clients[client_id][:session], client_id)
if message if message
websocket_send(client_id, message) websocket_send(client_id, message)

View file

@ -1,128 +1,428 @@
# encoding: utf-8 # encoding: utf-8
# rubocop:disable all
require 'browser_test_helper' require 'browser_test_helper'
class ChatTest < TestCase class ChatTest < TestCase
def test_websocket
return # TODO: temp disable def test_basic
message = 'message 1äöüß ' + rand(99_999_999_999_999_999).to_s agent = browser_instance
tests = [ login(
{ browser: agent,
name: 'start', username: 'master@example.com',
instance1: browser_instance, password: 'test',
instance2: browser_instance, url: browser_url,
instance1_username: 'master@example.com', )
instance1_password: 'test', tasks_close_all(
instance2_username: 'agent1@example.com', browser: agent,
instance2_password: 'test', )
url: browser_url,
action: [ # disable chat
{ click(
where: :instance1, browser: agent,
execute: 'check', css: 'a[href="#manage"]',
css: '#login', )
result: false, click(
}, browser: agent,
{ css: 'a[href="#channels/chat"]',
where: :instance2, )
execute: 'check', switch(
css: '#login', browser: agent,
result: false, css: '#content .js-chatSetting',
}, type: 'off',
{ )
execute: 'wait', sleep 25 # wait for rerendering
value: 1, click(
}, browser: agent,
{ css: 'a[href="#customer_chat"]',
where: :instance1, )
execute: 'click', match(
css: '#chat_toogle', browser: agent,
}, css: '.active.content',
{ value: 'disabled',
execute: 'wait', )
value: 8,
}, customer = browser_instance
{ location(
where: :instance1, browser: customer,
execute: 'click', url: "#{browser_url}/assets/chat/znuny.html",
css: '#chat_toogle', )
}, sleep 4
{ exists_not(
execute: 'wait', browser: customer,
value: 4, css: '.zammad-chat',
}, )
{ match(
where: :instance2, browser: customer,
execute: 'click', css: '.settings',
css: '#chat_toogle', value: '{"state":"chat_disabled"}',
}, )
{ click(
where: :instance1, browser: agent,
execute: 'click', css: 'a[href="#manage"]',
css: '#chat_toogle', )
}, click(
{ browser: agent,
execute: 'wait', css: 'a[href="#channels/chat"]',
value: 2, )
}, switch(
{ browser: agent,
where: :instance1, css: '#content .js-chatSetting',
execute: 'set', type: 'on',
css: 'input[name="chat_message"]', )
value: message, sleep 15 # wait for rerendering
}, switch(
{ browser: agent,
where: :instance1, css: '#navigation .js-switch',
execute: 'send_key', type: 'off',
css: 'input[name="chat_message"]', )
value: :enter, click(
}, browser: agent,
{ css: 'a[href="#customer_chat"]',
execute: 'wait', wait: 2,
value: 6, )
}, match_not(
{ browser: agent,
where: :instance1, css: '.active.content',
execute: 'match', value: 'disabled',
css: '#chat_log_container', )
value: message,
match_result: true, reload(
}, browser: customer,
{ )
where: :instance2, sleep 4
execute: 'match', exists_not(
css: '#chat_log_container', browser: customer,
value: message, css: '.zammad-chat',
match_result: true, )
}, match_not(
{ browser: customer,
where: :instance1, css: '.settings',
execute: 'navigate', value: '{"state":"chat_disabled"}',
to: browser_url, )
}, match(
{ browser: customer,
execute: 'wait', css: '.settings',
value: 8, value: '{"event":"chat_status_customer","data":{"state":"offline"}}',
}, )
{ click(
where: :instance1, browser: agent,
execute: 'click', css: 'a[href="#customer_chat"]',
css: '#chat_toogle', )
}, switch(
{ browser: agent,
execute: 'wait', css: '#navigation .js-switch',
value: 8, type: 'on',
}, )
{ reload(
where: :instance1, browser: customer,
execute: 'match', )
css: '#chat_log_container', watch_for(
value: message, browser: customer,
match_result: true, css: '.zammad-chat',
}, timeout: 5,
], )
}, match_not(
] browser: customer,
browser_double_test(tests) css: '.settings',
value: '{"state":"chat_disabled"}',
)
match_not(
browser: customer,
css: '.settings',
value: '{"event":"chat_status_customer","data":{"state":"offline"}}',
)
match(
browser: customer,
css: '.settings',
value: '"data":{"state":"online"}',
)
# init chat
click(
browser: customer,
css: '.js-chat-open',
)
exists(
browser: customer,
css: '.zammad-chat-is-shown',
)
watch_for(
browser: customer,
css: '.zammad-chat',
value: '(waiting|warte)',
)
watch_for(
browser: agent,
css: '.js-chatMenuItem .counter',
value: '1',
)
click(
browser: customer,
css: '.js-chat-close',
)
watch_for_disappear(
browser: customer,
css: '.zammad-chat',
value: '(waiting|warte)',
)
watch_for_disappear(
browser: agent,
css: '.js-chatMenuItem .counter',
)
end end
def test_basic_usecase1
agent = browser_instance
login(
browser: agent,
username: 'master@example.com',
password: 'test',
url: browser_url,
)
tasks_close_all(
browser: agent,
)
click(
browser: agent,
css: 'a[href="#customer_chat"]',
)
agent.find_elements( { css: '.active .chat-window .js-close' } ).each(&:click)
customer = browser_instance
location(
browser: customer,
url: "#{browser_url}/assets/chat/znuny.html",
)
watch_for(
browser: customer,
css: '.zammad-chat',
timeout: 5,
)
click(
browser: customer,
css: '.js-chat-open',
)
exists(
browser: customer,
css: '.zammad-chat-is-shown',
)
watch_for(
browser: customer,
css: '.zammad-chat',
value: '(waiting|warte)',
)
click(
browser: agent,
css: '.active .js-acceptChat',
)
sleep 2
exists_not(
browser: agent,
css: '.active .chat-window .chat-status.is-modified',
)
set(
browser: agent,
css: '.active .chat-window .js-customerChatInput',
value: 'my name is me',
)
click(
browser: agent,
css: '.active .chat-window .js-send',
)
watch_for(
browser: customer,
css: '.zammad-chat .zammad-chat-agent-status',
value: 'online',
)
watch_for(
browser: customer,
css: '.zammad-chat',
value: 'my name is me',
)
set(
browser: customer,
css: '.zammad-chat .zammad-chat-input',
value: 'my name is customer',
)
click(
browser: customer,
css: '.zammad-chat .zammad-chat-send',
)
watch_for(
browser: agent,
css: '.active .chat-window',
value: 'my name is customer',
)
exists(
browser: agent,
css: '.active .chat-window .chat-status.is-modified',
)
click(
browser: agent,
css: '.active .chat-window .js-customerChatInput',
)
exists_not(
browser: agent,
css: '.active .chat-window .chat-status.is-modified',
)
click(
browser: customer,
css: '.js-chat-close',
)
watch_for(
browser: agent,
css: '.active .chat-window',
value: 'has left the conversation',
)
end
def test_basic_usecase2
agent = browser_instance
login(
browser: agent,
username: 'master@example.com',
password: 'test',
url: browser_url,
)
tasks_close_all(
browser: agent,
)
click(
browser: agent,
css: 'a[href="#customer_chat"]',
)
agent.find_elements( { css: '.active .chat-window .js-close' } ).each(&:click)
customer = browser_instance
location(
browser: customer,
url: "#{browser_url}/assets/chat/znuny.html",
)
watch_for(
browser: customer,
css: '.zammad-chat',
timeout: 5,
)
click(
browser: customer,
css: '.js-chat-open',
)
exists(
browser: customer,
css: '.zammad-chat-is-shown',
)
watch_for(
browser: customer,
css: '.zammad-chat',
value: '(waiting|warte)',
)
click(
browser: agent,
css: '.active .js-acceptChat',
)
sleep 2
exists_not(
browser: agent,
css: '.active .chat-window .chat-status.is-modified',
)
watch_for(
browser: customer,
css: '.zammad-chat .zammad-chat-agent-status',
value: 'online',
)
set(
browser: customer,
css: '.zammad-chat .zammad-chat-input',
value: 'my name is customer',
)
click(
browser: customer,
css: '.zammad-chat .zammad-chat-send',
)
watch_for(
browser: agent,
css: '.active .chat-window',
value: 'my name is customer',
)
exists(
browser: agent,
css: '.active .chat-window .chat-status.is-modified',
)
set(
browser: agent,
css: '.active .chat-window .js-customerChatInput',
value: 'my name is me',
)
exists_not(
browser: agent,
css: '.active .chat-window .chat-status.is-modified',
)
click(
browser: agent,
css: '.active .chat-window .js-send',
)
watch_for(
browser: customer,
css: '.zammad-chat',
value: 'my name is me',
)
click(
browser: agent,
css: '.active .chat-window .js-close',
)
watch_for(
browser: customer,
css: '.zammad-chat .zammad-chat-agent-status',
value: 'offline',
)
watch_for(
browser: customer,
css: '.zammad-chat',
value: 'Chat closed by',
)
end
def test_timeouts
customer = browser_instance
location(
browser: customer,
url: "#{browser_url}/assets/chat/znuny.html",
)
watch_for(
browser: customer,
css: '.zammad-chat',
timeout: 5,
)
watch_for_disappear(
browser: customer,
css: '.zammad-chat',
timeout: 75,
)
reload(
browser: customer,
)
exists(
browser: customer,
css: '.zammad-chat',
)
click(
browser: customer,
css: '.js-chat-open',
)
watch_for(
browser: customer,
css: '.zammad-chat',
value: '(waiting|warte)',
timeout: 35,
)
watch_for(
browser: customer,
css: '.zammad-chat',
value: '(takes longer|dauert länger)',
timeout: 90,
)
end
end end