diff --git a/.codeclimate.yml b/.codeclimate.yml index 611533a7e..d0cde2858 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -11,3 +11,16 @@ plugins: enabled: true brakeman: enabled: true + +exclude_patterns: + - "spec/" + - "**/specs/" + - "db/*" + - "bin/**/*" + - "db/**/*" + - "config/**/*" + - "public/**/*" + - "vendor/**/*" + - "node_modules/**/*" + - "lib/tasks/auto_annotate_models.rake" + - "app/test-matchers.js" diff --git a/app/controllers/api/v1/widget/messages_controller.rb b/app/controllers/api/v1/widget/messages_controller.rb index e8fe9e813..69a84e31b 100644 --- a/app/controllers/api/v1/widget/messages_controller.rb +++ b/app/controllers/api/v1/widget/messages_controller.rb @@ -28,7 +28,7 @@ class Api::V1::Widget::MessagesController < ActionController::Base account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :incoming, - content: permitted_params[:content] + content: permitted_params[:message][:content] } end @@ -65,7 +65,8 @@ class Api::V1::Widget::MessagesController < ActionController::Base def message_finder_params { - filter_internal_messages: true + filter_internal_messages: true, + before: permitted_params[:before] } end @@ -78,7 +79,7 @@ class Api::V1::Widget::MessagesController < ActionController::Base end def permitted_params - params.fetch(:message).permit(:content) + params.permit(:before, message: [:content]) end def secret_key diff --git a/app/javascript/dashboard/assets/scss/_mixins.scss b/app/javascript/dashboard/assets/scss/_mixins.scss index bb9b23f4a..0b7a73c2c 100644 --- a/app/javascript/dashboard/assets/scss/_mixins.scss +++ b/app/javascript/dashboard/assets/scss/_mixins.scss @@ -149,7 +149,9 @@ @mixin color-spinner() { @keyframes spinner { - to {transform: rotate(360deg);} + to { + transform: rotate(360deg); + } } &:before { diff --git a/app/javascript/shared/components/Spinner.vue b/app/javascript/shared/components/Spinner.vue new file mode 100644 index 000000000..6f34313e5 --- /dev/null +++ b/app/javascript/shared/components/Spinner.vue @@ -0,0 +1,67 @@ + + + diff --git a/app/javascript/shared/components/specs/Spinner.spec.js b/app/javascript/shared/components/specs/Spinner.spec.js new file mode 100644 index 000000000..caf24c1ed --- /dev/null +++ b/app/javascript/shared/components/specs/Spinner.spec.js @@ -0,0 +1,10 @@ +import { mount } from '@vue/test-utils'; +import Spinner from '../Spinner'; + +describe('Spinner', () => { + test('matches snapshot', () => { + const wrapper = mount(Spinner); + expect(wrapper.isVueInstance()).toBeTruthy(); + expect(wrapper.element).toMatchSnapshot(); + }); +}); diff --git a/app/javascript/shared/components/specs/__snapshots__/Spinner.spec.js.snap b/app/javascript/shared/components/specs/__snapshots__/Spinner.spec.js.snap new file mode 100644 index 000000000..4c2f77a5d --- /dev/null +++ b/app/javascript/shared/components/specs/__snapshots__/Spinner.spec.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Spinner matches snapshot 1`] = ` + +`; diff --git a/app/javascript/widget/api/conversation.js b/app/javascript/widget/api/conversation.js index d4eb2f509..b7a243f67 100755 --- a/app/javascript/widget/api/conversation.js +++ b/app/javascript/widget/api/conversation.js @@ -7,9 +7,9 @@ const sendMessageAPI = async content => { return result; }; -const getConversationAPI = async conversationId => { - const urlData = endPoints.getConversation(conversationId); - const result = await API.get(urlData.url); +const getConversationAPI = async ({ before }) => { + const urlData = endPoints.getConversation({ before }); + const result = await API.get(urlData.url, { params: urlData.params }); return result; }; diff --git a/app/javascript/widget/api/endPoints.js b/app/javascript/widget/api/endPoints.js index d933da581..7933edea7 100755 --- a/app/javascript/widget/api/endPoints.js +++ b/app/javascript/widget/api/endPoints.js @@ -7,8 +7,9 @@ const sendMessage = content => ({ }, }); -const getConversation = () => ({ +const getConversation = ({ before }) => ({ url: `/api/v1/widget/messages${window.location.search}`, + params: { before }, }); export default { diff --git a/app/javascript/widget/api/specs/endPoints.spec.js b/app/javascript/widget/api/specs/endPoints.spec.js new file mode 100644 index 000000000..58663d448 --- /dev/null +++ b/app/javascript/widget/api/specs/endPoints.spec.js @@ -0,0 +1,25 @@ +import endPoints from '../endPoints'; + +describe('#sendMessage', () => { + it('returns correct payload', () => { + expect(endPoints.sendMessage('hello')).toEqual({ + url: `/api/v1/widget/messages`, + params: { + message: { + content: 'hello', + }, + }, + }); + }); +}); + +describe('#getConversation', () => { + it('returns correct payload', () => { + expect(endPoints.getConversation({ before: 123 })).toEqual({ + url: `/api/v1/widget/messages`, + params: { + before: 123, + }, + }); + }); +}); diff --git a/app/javascript/widget/components/ChatSendButton.vue b/app/javascript/widget/components/ChatSendButton.vue index 7e43c4028..f7bc77ed3 100755 --- a/app/javascript/widget/components/ChatSendButton.vue +++ b/app/javascript/widget/components/ChatSendButton.vue @@ -13,7 +13,7 @@ - - - diff --git a/app/javascript/widget/store/modules/conversation.js b/app/javascript/widget/store/modules/conversation.js index 6f1f25953..d00d9a820 100755 --- a/app/javascript/widget/store/modules/conversation.js +++ b/app/javascript/widget/store/modules/conversation.js @@ -21,42 +21,54 @@ export const findUndeliveredMessage = (messageInbox, { content }) => ); export const DEFAULT_CONVERSATION = 'default'; + const state = { conversations: {}, + uiFlags: { + allMessagesLoaded: false, + isFetchingList: false, + }, }; -const getters = { +export const getters = { getConversation: _state => _state.conversations, getConversationSize: _state => Object.keys(_state.conversations).length, + getAllMessagesLoaded: _state => _state.uiFlags.allMessagesLoaded, + getIsFetchingList: _state => _state.uiFlags.isFetchingList, + getEarliestMessage: _state => { + const conversation = Object.values(_state.conversations); + if (conversation.length) { + return conversation[0]; + } + return {}; + }, }; -const actions = { +export const actions = { sendMessage: async ({ commit }, params) => { const { content } = params; - commit('pushMessageToConversations', createTemporaryMessage(content)); + commit('pushMessageToConversation', createTemporaryMessage(content)); await sendMessageAPI(content); }, - fetchOldConversations: async ({ commit }) => { + fetchOldConversations: async ({ commit }, { before } = {}) => { try { - const { data } = await getConversationAPI(); - commit('initMessagesInConversation', data); + commit('setConversationListLoading', true); + const { data } = await getConversationAPI({ before }); + commit('setMessagesInConversation', data); + commit('setConversationListLoading', false); } catch (error) { - // Handle error + commit('setConversationListLoading', false); } }, addMessage({ commit }, data) { - commit('pushMessageToConversations', data); + commit('pushMessageToConversation', data); }, }; -const mutations = { - initInboxInConversations($state, lastConversation) { - Vue.set($state.conversations, lastConversation, {}); - }, - - pushMessageToConversations($state, message) { +export const mutations = { + pushMessageToConversation($state, message) { const { id, status, message_type: type } = message; const messagesInbox = $state.conversations; const isMessageIncoming = type === MESSAGE_TYPE.INCOMING; @@ -71,7 +83,6 @@ const mutations = { messagesInbox, message ); - if (!messageInConversation) { Vue.set(messagesInbox, id, message); } else { @@ -80,12 +91,17 @@ const mutations = { } }, - initMessagesInConversation(_state, payload) { + setConversationListLoading($state, status) { + $state.uiFlags.isFetchingList = status; + }, + + setMessagesInConversation($state, payload) { if (!payload.length) { + $state.uiFlags.allMessagesLoaded = true; return; } - payload.map(message => Vue.set(_state.conversations, message.id, message)); + payload.map(message => Vue.set($state.conversations, message.id, message)); }, }; diff --git a/app/javascript/widget/store/modules/specs/conversation/actions.spec.js b/app/javascript/widget/store/modules/specs/conversation/actions.spec.js new file mode 100644 index 000000000..1cb8075c1 --- /dev/null +++ b/app/javascript/widget/store/modules/specs/conversation/actions.spec.js @@ -0,0 +1,32 @@ +import { actions } from '../../conversation'; +import getUuid from '../../../../helpers/uuid'; + +jest.mock('../../../../helpers/uuid'); + +const commit = jest.fn(); + +describe('#actions', () => { + describe('#addMessage', () => { + it('sends correct mutations', () => { + actions.addMessage({ commit }, { id: 1 }); + expect(commit).toBeCalledWith('pushMessageToConversation', { id: 1 }); + }); + }); + + describe('#sendMessage', () => { + it('sends correct mutations', () => { + const mockDate = new Date(1466424490000); + getUuid.mockImplementationOnce(() => '1111'); + const spy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate); + actions.sendMessage({ commit }, { content: 'hello' }); + spy.mockRestore(); + expect(commit).toBeCalledWith('pushMessageToConversation', { + id: '1111', + content: 'hello', + status: 'in_progress', + created_at: 1466424490000, + message_type: 0, + }); + }); + }); +}); diff --git a/app/javascript/widget/store/modules/specs/conversation/getters.spec.js b/app/javascript/widget/store/modules/specs/conversation/getters.spec.js new file mode 100644 index 000000000..93d0729a4 --- /dev/null +++ b/app/javascript/widget/store/modules/specs/conversation/getters.spec.js @@ -0,0 +1,56 @@ +import { getters } from '../../conversation'; + +describe('#getters', () => { + it('getConversation', () => { + const state = { + conversations: { + 1: { + content: 'hello', + }, + }, + }; + expect(getters.getConversation(state)).toEqual({ + 1: { + content: 'hello', + }, + }); + }); + + it('getConversationSize', () => { + const state = { + conversations: { + 1: { + content: 'hello', + }, + }, + }; + expect(getters.getConversationSize(state)).toEqual(1); + }); + + it('getEarliestMessage', () => { + const state = { + conversations: { + 1: { + content: 'hello', + }, + 2: { + content: 'hello1', + }, + }, + }; + expect(getters.getEarliestMessage(state)).toEqual({ + content: 'hello', + }); + }); + + it('uiFlags', () => { + const state = { + uiFlags: { + allMessagesLoaded: false, + isFetchingList: false, + }, + }; + expect(getters.getAllMessagesLoaded(state)).toEqual(false); + expect(getters.getIsFetchingList(state)).toEqual(false); + }); +}); diff --git a/app/javascript/widget/store/modules/specs/conversation/mutations.spec.js b/app/javascript/widget/store/modules/specs/conversation/mutations.spec.js new file mode 100644 index 000000000..b465ac92e --- /dev/null +++ b/app/javascript/widget/store/modules/specs/conversation/mutations.spec.js @@ -0,0 +1,95 @@ +import { mutations } from '../../conversation'; + +const temporaryMessagePayload = { + content: 'hello', + id: 1, + message_type: 0, + status: 'in_progress', +}; + +const incomingMessagePayload = { + content: 'hello', + id: 1, + message_type: 0, + status: 'sent', +}; + +const outgoingMessagePayload = { + content: 'hello', + id: 1, + message_type: 1, + status: 'sent', +}; + +describe('#mutations', () => { + describe('#pushMessageToConversation', () => { + it('add message to conversation if outgoing', () => { + const state = { conversations: {} }; + mutations.pushMessageToConversation(state, outgoingMessagePayload); + expect(state.conversations).toEqual({ + 1: outgoingMessagePayload, + }); + }); + + it('add message to conversation if message in undelivered', () => { + const state = { conversations: {} }; + mutations.pushMessageToConversation(state, temporaryMessagePayload); + expect(state.conversations).toEqual({ + 1: temporaryMessagePayload, + }); + }); + + it('replaces temporary message in conversation with actual message', () => { + const state = { + conversations: { + rand_id_123: { + content: 'hello', + id: 'rand_id_123', + message_type: 0, + status: 'in_progress', + }, + }, + }; + mutations.pushMessageToConversation(state, incomingMessagePayload); + expect(state.conversations).toEqual({ + 1: incomingMessagePayload, + }); + }); + + it('adds message in conversation if it is a new message', () => { + const state = { conversations: {} }; + mutations.pushMessageToConversation(state, incomingMessagePayload); + expect(state.conversations).toEqual({ + 1: incomingMessagePayload, + }); + }); + }); + + describe('#setConversationListLoading', () => { + it('set status correctly', () => { + const state = { uiFlags: { isFetchingList: false } }; + mutations.setConversationListLoading(state, true); + expect(state.uiFlags.isFetchingList).toEqual(true); + }); + }); + + describe('#setMessagesInConversation', () => { + it('sets allMessagesLoaded flag if payload is empty', () => { + const state = { uiFlags: { allMessagesLoaded: false } }; + mutations.setMessagesInConversation(state, []); + expect(state.uiFlags.allMessagesLoaded).toEqual(true); + }); + + it('sets messages if payload is not empty', () => { + const state = { + uiFlags: { allMessagesLoaded: false }, + conversations: {}, + }; + mutations.setMessagesInConversation(state, [{ id: 1, content: 'hello' }]); + expect(state.conversations).toEqual({ + 1: { id: 1, content: 'hello' }, + }); + expect(state.uiFlags.allMessagesLoaded).toEqual(false); + }); + }); +}); diff --git a/app/javascript/widget/store/modules/specs/conversation.spec.js b/app/javascript/widget/store/modules/specs/conversation/utils.spec.js similarity index 96% rename from app/javascript/widget/store/modules/specs/conversation.spec.js rename to app/javascript/widget/store/modules/specs/conversation/utils.spec.js index 03a97618b..f7843de5c 100644 --- a/app/javascript/widget/store/modules/specs/conversation.spec.js +++ b/app/javascript/widget/store/modules/specs/conversation/utils.spec.js @@ -1,7 +1,7 @@ import { findUndeliveredMessage, createTemporaryMessage, -} from '../conversation'; +} from '../../conversation'; describe('#findUndeliveredMessage', () => { it('returns message objects if exist', () => {