Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { getAvailableAgents } from 'widget/api/agent';
|
||||
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
|
||||
import { getFromCache, setCache } from 'shared/helpers/cache';
|
||||
|
||||
const state = {
|
||||
records: [],
|
||||
uiFlags: {
|
||||
isError: false,
|
||||
hasFetched: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
getHasFetched: $state => $state.uiFlags.hasFetched,
|
||||
availableAgents: $state =>
|
||||
$state.records.filter(agent => agent.availability_status === 'online'),
|
||||
};
|
||||
|
||||
const CACHE_KEY_PREFIX = 'chatwoot_available_agents_';
|
||||
|
||||
export const actions = {
|
||||
fetchAvailableAgents: async ({ commit }, websiteToken) => {
|
||||
try {
|
||||
const cachedData = getFromCache(`${CACHE_KEY_PREFIX}${websiteToken}`);
|
||||
if (cachedData) {
|
||||
commit('setAgents', cachedData);
|
||||
commit('setError', false);
|
||||
commit('setHasFetched', true);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await getAvailableAgents(websiteToken);
|
||||
const { payload = [] } = data;
|
||||
setCache(`${CACHE_KEY_PREFIX}${websiteToken}`, payload);
|
||||
commit('setAgents', payload);
|
||||
commit('setError', false);
|
||||
commit('setHasFetched', true);
|
||||
} catch (error) {
|
||||
commit('setError', true);
|
||||
commit('setHasFetched', true);
|
||||
}
|
||||
},
|
||||
updatePresence: async ({ commit }, data) => {
|
||||
commit('updatePresence', data);
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
setAgents($state, data) {
|
||||
$state.records = data;
|
||||
},
|
||||
updatePresence: MutationHelpers.updatePresence,
|
||||
setError($state, value) {
|
||||
$state.uiFlags.isError = value;
|
||||
},
|
||||
setHasFetched($state, value) {
|
||||
$state.uiFlags.hasFetched = value;
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
};
|
||||
@@ -0,0 +1,155 @@
|
||||
import {
|
||||
SET_BUBBLE_VISIBILITY,
|
||||
SET_COLOR_SCHEME,
|
||||
SET_REFERRER_HOST,
|
||||
SET_WIDGET_APP_CONFIG,
|
||||
SET_WIDGET_COLOR,
|
||||
TOGGLE_WIDGET_OPEN,
|
||||
SET_ROUTE_UPDATE_STATE,
|
||||
} from '../types';
|
||||
|
||||
const state = {
|
||||
hideMessageBubble: false,
|
||||
isCampaignViewClicked: false,
|
||||
showUnreadMessagesDialog: true,
|
||||
isWebWidgetTriggered: false,
|
||||
isWidgetOpen: false,
|
||||
position: 'right',
|
||||
referrerHost: '',
|
||||
showPopoutButton: false,
|
||||
widgetColor: '',
|
||||
widgetStyle: 'standard',
|
||||
darkMode: 'light',
|
||||
isUpdatingRoute: false,
|
||||
welcomeTitle: '',
|
||||
welcomeDescription: '',
|
||||
availableMessage: '',
|
||||
unavailableMessage: '',
|
||||
enableFileUpload: undefined,
|
||||
enableEmojiPicker: true,
|
||||
enableEndConversation: true,
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
getAppConfig: $state => $state,
|
||||
isRightAligned: $state => $state.position === 'right',
|
||||
getHideMessageBubble: $state => $state.hideMessageBubble,
|
||||
getIsWidgetOpen: $state => $state.isWidgetOpen,
|
||||
getWidgetColor: $state => $state.widgetColor,
|
||||
getReferrerHost: $state => $state.referrerHost,
|
||||
isWidgetStyleFlat: $state => $state.widgetStyle === 'flat',
|
||||
darkMode: $state => $state.darkMode,
|
||||
getShowUnreadMessagesDialog: $state => $state.showUnreadMessagesDialog,
|
||||
getIsUpdatingRoute: _state => _state.isUpdatingRoute,
|
||||
getWelcomeHeading: $state => $state.welcomeTitle,
|
||||
getWelcomeTagline: $state => $state.welcomeDescription,
|
||||
getAvailableMessage: $state => $state.availableMessage,
|
||||
getUnavailableMessage: $state => $state.unavailableMessage,
|
||||
getShouldShowFilePicker: $state => $state.enableFileUpload,
|
||||
getShouldShowEmojiPicker: $state => $state.enableEmojiPicker,
|
||||
getCanUserEndConversation: $state => $state.enableEndConversation,
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
setAppConfig(
|
||||
{ commit },
|
||||
{
|
||||
showPopoutButton,
|
||||
position,
|
||||
hideMessageBubble,
|
||||
showUnreadMessagesDialog,
|
||||
widgetStyle = 'rounded',
|
||||
darkMode = 'light',
|
||||
welcomeTitle = '',
|
||||
welcomeDescription = '',
|
||||
availableMessage = '',
|
||||
unavailableMessage = '',
|
||||
enableFileUpload = undefined,
|
||||
enableEmojiPicker = true,
|
||||
enableEndConversation = true,
|
||||
}
|
||||
) {
|
||||
commit(SET_WIDGET_APP_CONFIG, {
|
||||
hideMessageBubble: !!hideMessageBubble,
|
||||
position: position || 'right',
|
||||
showPopoutButton: !!showPopoutButton,
|
||||
showUnreadMessagesDialog: !!showUnreadMessagesDialog,
|
||||
widgetStyle,
|
||||
darkMode,
|
||||
welcomeTitle,
|
||||
welcomeDescription,
|
||||
availableMessage,
|
||||
unavailableMessage,
|
||||
enableFileUpload,
|
||||
enableEmojiPicker,
|
||||
enableEndConversation,
|
||||
});
|
||||
},
|
||||
toggleWidgetOpen({ commit }, isWidgetOpen) {
|
||||
commit(TOGGLE_WIDGET_OPEN, isWidgetOpen);
|
||||
},
|
||||
setWidgetColor({ commit }, widgetColor) {
|
||||
commit(SET_WIDGET_COLOR, widgetColor);
|
||||
},
|
||||
setColorScheme({ commit }, darkMode) {
|
||||
commit(SET_COLOR_SCHEME, darkMode);
|
||||
},
|
||||
setReferrerHost({ commit }, referrerHost) {
|
||||
commit(SET_REFERRER_HOST, referrerHost);
|
||||
},
|
||||
setBubbleVisibility({ commit }, hideMessageBubble) {
|
||||
commit(SET_BUBBLE_VISIBILITY, hideMessageBubble);
|
||||
},
|
||||
setRouteTransitionState: async ({ commit }, status) => {
|
||||
// Handles the routing state during navigation to different screen
|
||||
// Called before the navigation starts and after navigation completes
|
||||
// Handling this state in app/javascript/widget/router.js
|
||||
// See issue: https://github.com/chatwoot/chatwoot/issues/10736
|
||||
commit(SET_ROUTE_UPDATE_STATE, status);
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
[SET_WIDGET_APP_CONFIG]($state, data) {
|
||||
$state.showPopoutButton = data.showPopoutButton;
|
||||
$state.position = data.position;
|
||||
$state.hideMessageBubble = data.hideMessageBubble;
|
||||
$state.widgetStyle = data.widgetStyle;
|
||||
$state.darkMode = data.darkMode;
|
||||
$state.locale = data.locale || $state.locale;
|
||||
$state.showUnreadMessagesDialog = data.showUnreadMessagesDialog;
|
||||
$state.welcomeTitle = data.welcomeTitle;
|
||||
$state.welcomeDescription = data.welcomeDescription;
|
||||
$state.availableMessage = data.availableMessage;
|
||||
$state.unavailableMessage = data.unavailableMessage;
|
||||
$state.enableFileUpload = data.enableFileUpload;
|
||||
$state.enableEmojiPicker = data.enableEmojiPicker;
|
||||
$state.enableEndConversation = data.enableEndConversation;
|
||||
},
|
||||
[TOGGLE_WIDGET_OPEN]($state, isWidgetOpen) {
|
||||
$state.isWidgetOpen = isWidgetOpen;
|
||||
},
|
||||
[SET_WIDGET_COLOR]($state, widgetColor) {
|
||||
$state.widgetColor = widgetColor;
|
||||
},
|
||||
[SET_REFERRER_HOST]($state, referrerHost) {
|
||||
$state.referrerHost = referrerHost;
|
||||
},
|
||||
[SET_BUBBLE_VISIBILITY]($state, hideMessageBubble) {
|
||||
$state.hideMessageBubble = hideMessageBubble;
|
||||
},
|
||||
[SET_COLOR_SCHEME]($state, darkMode) {
|
||||
$state.darkMode = darkMode;
|
||||
},
|
||||
[SET_ROUTE_UPDATE_STATE]($state, status) {
|
||||
$state.isUpdatingRoute = status;
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import { getMostReadArticles } from 'widget/api/article';
|
||||
import { getFromCache, setCache } from 'shared/helpers/cache';
|
||||
|
||||
const CACHE_KEY_PREFIX = 'chatwoot_most_read_articles_';
|
||||
|
||||
const state = {
|
||||
records: [],
|
||||
uiFlags: {
|
||||
isError: false,
|
||||
hasFetched: false,
|
||||
isFetching: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
uiFlags: $state => $state.uiFlags,
|
||||
popularArticles: $state => $state.records,
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
fetch: async ({ commit }, { slug, locale }) => {
|
||||
commit('setIsFetching', true);
|
||||
commit('setError', false);
|
||||
|
||||
try {
|
||||
if (!locale) return;
|
||||
const cachedData = getFromCache(`${CACHE_KEY_PREFIX}${slug}_${locale}`);
|
||||
if (cachedData) {
|
||||
commit('setArticles', cachedData);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await getMostReadArticles(slug, locale);
|
||||
const { payload = [] } = data;
|
||||
|
||||
setCache(`${CACHE_KEY_PREFIX}${slug}_${locale}`, payload);
|
||||
if (payload.length) {
|
||||
commit('setArticles', payload);
|
||||
}
|
||||
} catch (error) {
|
||||
commit('setError', true);
|
||||
} finally {
|
||||
commit('setIsFetching', false);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
setArticles($state, data) {
|
||||
$state.records = data;
|
||||
},
|
||||
setError($state, value) {
|
||||
$state.uiFlags.isError = value;
|
||||
},
|
||||
setIsFetching($state, value) {
|
||||
$state.uiFlags.isFetching = value;
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
};
|
||||
@@ -0,0 +1,183 @@
|
||||
import { getCampaigns, triggerCampaign } from 'widget/api/campaign';
|
||||
import campaignTimer from 'widget/helpers/campaignTimer';
|
||||
import {
|
||||
formatCampaigns,
|
||||
filterCampaigns,
|
||||
} from 'widget/helpers/campaignHelper';
|
||||
import { getFromCache, setCache } from 'shared/helpers/cache';
|
||||
|
||||
const CACHE_KEY_PREFIX = 'chatwoot_campaigns_';
|
||||
|
||||
const state = {
|
||||
records: [],
|
||||
uiFlags: {
|
||||
hasFetched: false,
|
||||
isError: false,
|
||||
},
|
||||
activeCampaign: {},
|
||||
};
|
||||
|
||||
const resetCampaignTimers = (
|
||||
campaigns,
|
||||
currentURL,
|
||||
websiteToken,
|
||||
isInBusinessHours
|
||||
) => {
|
||||
const formattedCampaigns = formatCampaigns({ campaigns });
|
||||
// Find all campaigns that matches the current URL
|
||||
const filteredCampaigns = filterCampaigns({
|
||||
campaigns: formattedCampaigns,
|
||||
currentURL,
|
||||
isInBusinessHours,
|
||||
});
|
||||
campaignTimer.initTimers({ campaigns: filteredCampaigns }, websiteToken);
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
getCampaigns: $state => $state.records,
|
||||
getUIFlags: $state => $state.uiFlags,
|
||||
getActiveCampaign: $state => $state.activeCampaign,
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
fetchCampaigns: async (
|
||||
{ commit },
|
||||
{ websiteToken, currentURL, isInBusinessHours }
|
||||
) => {
|
||||
try {
|
||||
// Cache for 1 hour
|
||||
const CACHE_EXPIRY = 60 * 60 * 1000;
|
||||
const cachedData = getFromCache(
|
||||
`${CACHE_KEY_PREFIX}${websiteToken}`,
|
||||
CACHE_EXPIRY
|
||||
);
|
||||
if (cachedData) {
|
||||
commit('setCampaigns', cachedData);
|
||||
commit('setError', false);
|
||||
resetCampaignTimers(
|
||||
cachedData,
|
||||
currentURL,
|
||||
websiteToken,
|
||||
isInBusinessHours
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: campaigns } = await getCampaigns(websiteToken);
|
||||
setCache(`${CACHE_KEY_PREFIX}${websiteToken}`, campaigns);
|
||||
commit('setCampaigns', campaigns);
|
||||
commit('setError', false);
|
||||
resetCampaignTimers(
|
||||
campaigns,
|
||||
currentURL,
|
||||
websiteToken,
|
||||
isInBusinessHours
|
||||
);
|
||||
} catch (error) {
|
||||
commit('setError', true);
|
||||
}
|
||||
},
|
||||
initCampaigns: async (
|
||||
{ getters: { getCampaigns: campaigns, getUIFlags: uiFlags }, dispatch },
|
||||
{ currentURL, websiteToken, isInBusinessHours }
|
||||
) => {
|
||||
if (!campaigns.length) {
|
||||
// This check is added to ensure that the campaigns are fetched once
|
||||
// On high traffic sites, if the campaigns are empty, the API is called
|
||||
// every time the user changes the URL (in case of the SPA)
|
||||
// So, we need to ensure that the campaigns are fetched only once
|
||||
if (!uiFlags.hasFetched) {
|
||||
dispatch('fetchCampaigns', {
|
||||
websiteToken,
|
||||
currentURL,
|
||||
isInBusinessHours,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
resetCampaignTimers(
|
||||
campaigns,
|
||||
currentURL,
|
||||
websiteToken,
|
||||
isInBusinessHours
|
||||
);
|
||||
}
|
||||
},
|
||||
startCampaign: async (
|
||||
{
|
||||
commit,
|
||||
rootState: {
|
||||
appConfig: { isWidgetOpen },
|
||||
},
|
||||
},
|
||||
{ websiteToken, campaignId }
|
||||
) => {
|
||||
// Disable campaign execution if widget is opened
|
||||
if (!isWidgetOpen) {
|
||||
const { data: campaigns } = await getCampaigns(websiteToken);
|
||||
// Check campaign is disabled or not
|
||||
const campaign = campaigns.find(item => item.id === campaignId);
|
||||
if (campaign) {
|
||||
commit('setActiveCampaign', campaign);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
executeCampaign: async (
|
||||
{ commit },
|
||||
{ campaignId, websiteToken, customAttributes }
|
||||
) => {
|
||||
try {
|
||||
commit(
|
||||
'conversation/setConversationUIFlag',
|
||||
{ isCreating: true },
|
||||
{ root: true }
|
||||
);
|
||||
await triggerCampaign({ campaignId, websiteToken, customAttributes });
|
||||
commit('setCampaignExecuted', true);
|
||||
commit('setActiveCampaign', {});
|
||||
} catch (error) {
|
||||
commit('setError', true);
|
||||
} finally {
|
||||
commit(
|
||||
'conversation/setConversationUIFlag',
|
||||
{ isCreating: false },
|
||||
{ root: true }
|
||||
);
|
||||
}
|
||||
},
|
||||
resetCampaign: async ({ commit }) => {
|
||||
try {
|
||||
commit('setCampaignExecuted', false);
|
||||
commit('setActiveCampaign', {});
|
||||
} catch (error) {
|
||||
commit('setError', true);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
setCampaigns($state, data) {
|
||||
$state.records = data;
|
||||
$state.uiFlags.hasFetched = true;
|
||||
},
|
||||
setActiveCampaign($state, data) {
|
||||
$state.activeCampaign = data;
|
||||
},
|
||||
setError($state, value) {
|
||||
$state.uiFlags.isError = value;
|
||||
},
|
||||
setHasFetched($state, value) {
|
||||
$state.uiFlags.hasFetched = value;
|
||||
},
|
||||
setCampaignExecuted($state, data) {
|
||||
$state.campaignHasExecuted = data;
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
import { sendMessage } from 'widget/helpers/utils';
|
||||
import ContactsAPI from '../../api/contacts';
|
||||
import { SET_USER_ERROR } from '../../constants/errorTypes';
|
||||
import { setHeader } from '../../helpers/axios';
|
||||
const state = {
|
||||
currentUser: {},
|
||||
};
|
||||
|
||||
const SET_CURRENT_USER = 'SET_CURRENT_USER';
|
||||
const parseErrorData = error =>
|
||||
error && error.response && error.response.data ? error.response.data : error;
|
||||
export const updateWidgetAuthToken = widgetAuthToken => {
|
||||
if (widgetAuthToken) {
|
||||
setHeader(widgetAuthToken);
|
||||
sendMessage({
|
||||
event: 'setAuthCookie',
|
||||
data: { widgetAuthToken },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
getCurrentUser(_state) {
|
||||
return _state.currentUser;
|
||||
},
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
get: async ({ commit }) => {
|
||||
try {
|
||||
const { data } = await ContactsAPI.get();
|
||||
commit(SET_CURRENT_USER, data);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
}
|
||||
},
|
||||
update: async ({ dispatch }, { user }) => {
|
||||
try {
|
||||
await ContactsAPI.update(user);
|
||||
dispatch('get');
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
}
|
||||
},
|
||||
setUser: async ({ dispatch }, { identifier, user: userObject }) => {
|
||||
try {
|
||||
const {
|
||||
email,
|
||||
name,
|
||||
avatar_url,
|
||||
identifier_hash: identifierHash,
|
||||
phone_number,
|
||||
company_name,
|
||||
city,
|
||||
country_code,
|
||||
description,
|
||||
custom_attributes,
|
||||
social_profiles,
|
||||
} = userObject;
|
||||
const user = {
|
||||
email,
|
||||
name,
|
||||
avatar_url,
|
||||
identifier_hash: identifierHash,
|
||||
phone_number,
|
||||
additional_attributes: {
|
||||
company_name,
|
||||
city,
|
||||
description,
|
||||
country_code,
|
||||
social_profiles,
|
||||
},
|
||||
custom_attributes,
|
||||
};
|
||||
const {
|
||||
data: { widget_auth_token: widgetAuthToken },
|
||||
} = await ContactsAPI.setUser(identifier, user);
|
||||
updateWidgetAuthToken(widgetAuthToken);
|
||||
dispatch('get');
|
||||
if (identifierHash || widgetAuthToken) {
|
||||
dispatch('conversation/clearConversations', {}, { root: true });
|
||||
dispatch('conversation/fetchOldConversations', {}, { root: true });
|
||||
dispatch('conversationAttributes/getAttributes', {}, { root: true });
|
||||
}
|
||||
} catch (error) {
|
||||
const data = parseErrorData(error);
|
||||
sendMessage({ event: 'error', errorType: SET_USER_ERROR, data });
|
||||
}
|
||||
},
|
||||
setCustomAttributes: async (_, customAttributes = {}) => {
|
||||
try {
|
||||
await ContactsAPI.setCustomAttributes(customAttributes);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
}
|
||||
},
|
||||
deleteCustomAttribute: async (_, customAttribute) => {
|
||||
try {
|
||||
await ContactsAPI.deleteCustomAttribute(customAttribute);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
[SET_CURRENT_USER]($state, user) {
|
||||
const { currentUser } = $state;
|
||||
$state.currentUser = { ...currentUser, ...user };
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
};
|
||||
@@ -0,0 +1,198 @@
|
||||
import {
|
||||
createConversationAPI,
|
||||
sendMessageAPI,
|
||||
getMessagesAPI,
|
||||
sendAttachmentAPI,
|
||||
toggleTyping,
|
||||
setUserLastSeenAt,
|
||||
toggleStatus,
|
||||
setCustomAttributes,
|
||||
deleteCustomAttribute,
|
||||
} from 'widget/api/conversation';
|
||||
|
||||
import { ON_CONVERSATION_CREATED } from 'widget/constants/widgetBusEvents';
|
||||
import { createTemporaryMessage, getNonDeletedMessages } from './helpers';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
export const actions = {
|
||||
createConversation: async ({ commit, dispatch }, params) => {
|
||||
commit('setConversationUIFlag', { isCreating: true });
|
||||
try {
|
||||
const { data } = await createConversationAPI(params);
|
||||
const { messages } = data;
|
||||
const [message = {}] = messages;
|
||||
commit('pushMessageToConversation', message);
|
||||
dispatch('conversationAttributes/getAttributes', {}, { root: true });
|
||||
// Emit event to notify that conversation is created and show the chat screen
|
||||
emitter.emit(ON_CONVERSATION_CREATED);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
} finally {
|
||||
commit('setConversationUIFlag', { isCreating: false });
|
||||
}
|
||||
},
|
||||
sendMessage: async ({ dispatch }, params) => {
|
||||
const { content, replyTo } = params;
|
||||
const message = createTemporaryMessage({ content, replyTo });
|
||||
dispatch('sendMessageWithData', message);
|
||||
},
|
||||
sendMessageWithData: async ({ commit }, message) => {
|
||||
const { id, content, replyTo, meta = {} } = message;
|
||||
|
||||
commit('pushMessageToConversation', message);
|
||||
commit('updateMessageMeta', { id, meta: { ...meta, error: '' } });
|
||||
try {
|
||||
const { data } = await sendMessageAPI(content, replyTo);
|
||||
|
||||
// [VITE] Don't delete this manually, since `pushMessageToConversation` does the replacement for us anyway
|
||||
// commit('deleteMessage', message.id);
|
||||
commit('pushMessageToConversation', { ...data, status: 'sent' });
|
||||
} catch (error) {
|
||||
commit('pushMessageToConversation', { ...message, status: 'failed' });
|
||||
commit('updateMessageMeta', {
|
||||
id,
|
||||
meta: { ...meta, error: '' },
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setLastMessageId: async ({ commit }) => {
|
||||
commit('setLastMessageId');
|
||||
},
|
||||
|
||||
sendAttachment: async ({ commit }, params) => {
|
||||
const {
|
||||
attachment: { thumbUrl, fileType },
|
||||
meta = {},
|
||||
} = params;
|
||||
const attachment = {
|
||||
thumb_url: thumbUrl,
|
||||
data_url: thumbUrl,
|
||||
file_type: fileType,
|
||||
status: 'in_progress',
|
||||
};
|
||||
const tempMessage = createTemporaryMessage({
|
||||
attachments: [attachment],
|
||||
replyTo: params.replyTo,
|
||||
});
|
||||
commit('pushMessageToConversation', tempMessage);
|
||||
try {
|
||||
const { data } = await sendAttachmentAPI(params);
|
||||
commit('updateAttachmentMessageStatus', {
|
||||
message: data,
|
||||
tempId: tempMessage.id,
|
||||
});
|
||||
commit('pushMessageToConversation', { ...data, status: 'sent' });
|
||||
} catch (error) {
|
||||
commit('pushMessageToConversation', { ...tempMessage, status: 'failed' });
|
||||
commit('updateMessageMeta', {
|
||||
id: tempMessage.id,
|
||||
meta: { ...meta, error: '' },
|
||||
});
|
||||
// Show error
|
||||
}
|
||||
},
|
||||
fetchOldConversations: async ({ commit }, { before } = {}) => {
|
||||
try {
|
||||
commit('setConversationListLoading', true);
|
||||
const {
|
||||
data: { payload, meta },
|
||||
} = await getMessagesAPI({ before });
|
||||
const { contact_last_seen_at: lastSeen } = meta;
|
||||
const formattedMessages = getNonDeletedMessages({ messages: payload });
|
||||
commit('conversation/setMetaUserLastSeenAt', lastSeen, { root: true });
|
||||
commit('setMessagesInConversation', formattedMessages);
|
||||
commit('setConversationListLoading', false);
|
||||
} catch (error) {
|
||||
commit('setConversationListLoading', false);
|
||||
}
|
||||
},
|
||||
|
||||
syncLatestMessages: async ({ state, commit }) => {
|
||||
try {
|
||||
const { lastMessageId, conversations } = state;
|
||||
|
||||
const {
|
||||
data: { payload, meta },
|
||||
} = await getMessagesAPI({ after: lastMessageId });
|
||||
|
||||
const { contact_last_seen_at: lastSeen } = meta;
|
||||
const formattedMessages = getNonDeletedMessages({ messages: payload });
|
||||
const missingMessages = formattedMessages.filter(
|
||||
message => conversations?.[message.id] === undefined
|
||||
);
|
||||
if (!missingMessages.length) return;
|
||||
missingMessages.forEach(message => {
|
||||
conversations[message.id] = message;
|
||||
});
|
||||
// Sort conversation messages by created_at
|
||||
const updatedConversation = Object.fromEntries(
|
||||
Object.entries(conversations).sort(
|
||||
(a, b) => a[1].created_at - b[1].created_at
|
||||
)
|
||||
);
|
||||
commit('conversation/setMetaUserLastSeenAt', lastSeen, { root: true });
|
||||
commit('setMissingMessagesInConversation', updatedConversation);
|
||||
} catch (error) {
|
||||
// IgnoreError
|
||||
}
|
||||
},
|
||||
|
||||
clearConversations: ({ commit }) => {
|
||||
commit('clearConversations');
|
||||
},
|
||||
|
||||
addOrUpdateMessage: async ({ commit }, data) => {
|
||||
const { id, content_attributes } = data;
|
||||
if (content_attributes && content_attributes.deleted) {
|
||||
commit('deleteMessage', id);
|
||||
return;
|
||||
}
|
||||
commit('pushMessageToConversation', data);
|
||||
},
|
||||
|
||||
toggleAgentTyping({ commit }, data) {
|
||||
commit('toggleAgentTypingStatus', data);
|
||||
},
|
||||
|
||||
toggleUserTyping: async (_, data) => {
|
||||
try {
|
||||
await toggleTyping(data);
|
||||
} catch (error) {
|
||||
// IgnoreError
|
||||
}
|
||||
},
|
||||
|
||||
setUserLastSeen: async ({ commit, getters: appGetters }) => {
|
||||
if (!appGetters.getConversationSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastSeen = Date.now() / 1000;
|
||||
try {
|
||||
commit('setMetaUserLastSeenAt', lastSeen);
|
||||
await setUserLastSeenAt({ lastSeen });
|
||||
} catch (error) {
|
||||
// IgnoreError
|
||||
}
|
||||
},
|
||||
|
||||
resolveConversation: async () => {
|
||||
await toggleStatus();
|
||||
},
|
||||
|
||||
setCustomAttributes: async (_, customAttributes = {}) => {
|
||||
try {
|
||||
await setCustomAttributes(customAttributes);
|
||||
} catch (error) {
|
||||
// IgnoreError
|
||||
}
|
||||
},
|
||||
|
||||
deleteCustomAttribute: async (_, customAttribute) => {
|
||||
try {
|
||||
await deleteCustomAttribute(customAttribute);
|
||||
} catch (error) {
|
||||
// IgnoreError
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
||||
import { groupBy } from 'widget/helpers/utils';
|
||||
import { groupConversationBySender } from './helpers';
|
||||
import { formatUnixDate } from 'shared/helpers/DateHelper';
|
||||
|
||||
export const getters = {
|
||||
getAllMessagesLoaded: _state => _state.uiFlags.allMessagesLoaded,
|
||||
getIsCreating: _state => _state.uiFlags.isCreating,
|
||||
getIsAgentTyping: _state => _state.uiFlags.isAgentTyping,
|
||||
getConversation: _state => _state.conversations,
|
||||
getConversationSize: _state => Object.keys(_state.conversations).length,
|
||||
getEarliestMessage: _state => {
|
||||
const conversation = Object.values(_state.conversations);
|
||||
if (conversation.length) {
|
||||
return conversation[0];
|
||||
}
|
||||
return {};
|
||||
},
|
||||
getLastMessage: _state => {
|
||||
const conversation = Object.values(_state.conversations);
|
||||
if (conversation.length) {
|
||||
return conversation[conversation.length - 1];
|
||||
}
|
||||
return {};
|
||||
},
|
||||
getGroupedConversation: _state => {
|
||||
const conversationGroupedByDate = groupBy(
|
||||
Object.values(_state.conversations),
|
||||
message => formatUnixDate(message.created_at)
|
||||
);
|
||||
return Object.keys(conversationGroupedByDate).map(date => ({
|
||||
date,
|
||||
messages: groupConversationBySender(conversationGroupedByDate[date]),
|
||||
}));
|
||||
},
|
||||
getIsFetchingList: _state => _state.uiFlags.isFetchingList,
|
||||
getMessageCount: _state => {
|
||||
return Object.values(_state.conversations).length;
|
||||
},
|
||||
getUnreadMessageCount: _state => {
|
||||
const { userLastSeenAt } = _state.meta;
|
||||
return Object.values(_state.conversations).filter(chat => {
|
||||
const { created_at: createdAt, message_type: messageType } = chat;
|
||||
const isOutGoing = messageType === MESSAGE_TYPE.OUTGOING;
|
||||
const hasNotSeen = userLastSeenAt
|
||||
? createdAt * 1000 > userLastSeenAt * 1000
|
||||
: true;
|
||||
return hasNotSeen && isOutGoing;
|
||||
}).length;
|
||||
},
|
||||
getUnreadTextMessages: (_state, _getters) => {
|
||||
const unreadCount = _getters.getUnreadMessageCount;
|
||||
const allMessages = [...Object.values(_state.conversations)];
|
||||
const unreadAgentMessages = allMessages.filter(message => {
|
||||
const { message_type: messageType } = message;
|
||||
return messageType === MESSAGE_TYPE.OUTGOING;
|
||||
});
|
||||
const maxUnreadCount = Math.min(unreadCount, 3);
|
||||
return unreadAgentMessages.splice(-maxUnreadCount);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
||||
import { isASubmittedFormMessage } from 'shared/helpers/MessageTypeHelper';
|
||||
|
||||
import getUuid from '../../../helpers/uuid';
|
||||
export const createTemporaryMessage = ({ attachments, content, replyTo }) => {
|
||||
const timestamp = new Date().getTime() / 1000;
|
||||
return {
|
||||
id: getUuid(),
|
||||
content,
|
||||
attachments,
|
||||
status: 'in_progress',
|
||||
replyTo,
|
||||
created_at: timestamp,
|
||||
message_type: MESSAGE_TYPE.INCOMING,
|
||||
};
|
||||
};
|
||||
|
||||
const getSenderName = message => (message.sender ? message.sender.name : '');
|
||||
|
||||
const shouldShowAvatar = (message, nextMessage) => {
|
||||
const currentSender = getSenderName(message);
|
||||
const nextSender = getSenderName(nextMessage);
|
||||
|
||||
return (
|
||||
currentSender !== nextSender ||
|
||||
message.message_type !== nextMessage.message_type ||
|
||||
isASubmittedFormMessage(nextMessage)
|
||||
);
|
||||
};
|
||||
|
||||
export const groupConversationBySender = conversationsForADate =>
|
||||
conversationsForADate.map((message, index) => {
|
||||
let showAvatar;
|
||||
const isLastMessage = index === conversationsForADate.length - 1;
|
||||
if (isASubmittedFormMessage(message)) {
|
||||
showAvatar = false;
|
||||
} else if (isLastMessage) {
|
||||
showAvatar = true;
|
||||
} else {
|
||||
const nextMessage = conversationsForADate[index + 1];
|
||||
showAvatar = shouldShowAvatar(message, nextMessage);
|
||||
}
|
||||
return { showAvatar, ...message };
|
||||
});
|
||||
|
||||
export const findUndeliveredMessage = (messageInbox, { content }) =>
|
||||
Object.values(messageInbox).filter(
|
||||
message => message.content === content && message.status === 'in_progress'
|
||||
);
|
||||
|
||||
export const getNonDeletedMessages = ({ messages }) => {
|
||||
return messages.filter(
|
||||
item => !(item.content_attributes && item.content_attributes.deleted)
|
||||
);
|
||||
};
|
||||
25
research/chatwoot/app/javascript/widget/store/modules/conversation/index.js
Executable file
25
research/chatwoot/app/javascript/widget/store/modules/conversation/index.js
Executable file
@@ -0,0 +1,25 @@
|
||||
import { getters } from './getters';
|
||||
import { actions } from './actions';
|
||||
import { mutations } from './mutations';
|
||||
|
||||
const state = {
|
||||
conversations: {},
|
||||
meta: {
|
||||
userLastSeenAt: undefined,
|
||||
},
|
||||
uiFlags: {
|
||||
allMessagesLoaded: false,
|
||||
isFetchingList: false,
|
||||
isAgentTyping: false,
|
||||
isCreating: false,
|
||||
},
|
||||
lastMessageId: null,
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
||||
import { findUndeliveredMessage } from './helpers';
|
||||
|
||||
export const mutations = {
|
||||
clearConversations($state) {
|
||||
$state.conversations = {};
|
||||
},
|
||||
pushMessageToConversation($state, message) {
|
||||
const { id, status, message_type: type } = message;
|
||||
|
||||
const messagesInbox = $state.conversations;
|
||||
const isMessageIncoming = type === MESSAGE_TYPE.INCOMING;
|
||||
const isTemporaryMessage = status === 'in_progress';
|
||||
|
||||
if (!isMessageIncoming || isTemporaryMessage) {
|
||||
messagesInbox[id] = message;
|
||||
return;
|
||||
}
|
||||
|
||||
const [messageInConversation] = findUndeliveredMessage(
|
||||
messagesInbox,
|
||||
message
|
||||
);
|
||||
if (!messageInConversation) {
|
||||
messagesInbox[id] = message;
|
||||
} else {
|
||||
// [VITE] instead of leaving undefined behind, we remove it completely
|
||||
// remove the temporary message and replace it with the new message
|
||||
// messagesInbox[messageInConversation.id] = undefined;
|
||||
delete messagesInbox[messageInConversation.id];
|
||||
messagesInbox[id] = message;
|
||||
}
|
||||
},
|
||||
|
||||
updateAttachmentMessageStatus($state, { message, tempId }) {
|
||||
const { id } = message;
|
||||
const messagesInbox = $state.conversations;
|
||||
|
||||
const messageInConversation = messagesInbox[tempId];
|
||||
|
||||
if (messageInConversation) {
|
||||
// [VITE] instead of leaving undefined behind, we remove it completely
|
||||
// remove the temporary message and replace it with the new message
|
||||
// messagesInbox[tempId] = undefined;
|
||||
delete messagesInbox[tempId];
|
||||
messagesInbox[id] = { ...message };
|
||||
}
|
||||
},
|
||||
|
||||
setConversationUIFlag($state, uiFlags) {
|
||||
$state.uiFlags = {
|
||||
...$state.uiFlags,
|
||||
...uiFlags,
|
||||
};
|
||||
},
|
||||
|
||||
setConversationListLoading($state, status) {
|
||||
$state.uiFlags.isFetchingList = status;
|
||||
},
|
||||
|
||||
setMessagesInConversation($state, payload) {
|
||||
if (!payload.length) {
|
||||
$state.uiFlags.allMessagesLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
payload.forEach(message => {
|
||||
$state.conversations[message.id] = message;
|
||||
});
|
||||
},
|
||||
|
||||
setMissingMessagesInConversation($state, payload) {
|
||||
$state.conversation = payload;
|
||||
},
|
||||
|
||||
updateMessage($state, { id, content_attributes }) {
|
||||
$state.conversations[id] = {
|
||||
...$state.conversations[id],
|
||||
content_attributes: {
|
||||
...($state.conversations[id].content_attributes || {}),
|
||||
...content_attributes,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
updateMessageMeta($state, { id, meta }) {
|
||||
const message = $state.conversations[id];
|
||||
if (!message) return;
|
||||
|
||||
const newMeta = message.meta ? { ...message.meta, ...meta } : { ...meta };
|
||||
message.meta = { ...newMeta };
|
||||
},
|
||||
|
||||
deleteMessage($state, id) {
|
||||
delete $state.conversations[id];
|
||||
// [VITE] In Vue 3 proxy objects, we can't delete properties by setting them to undefined
|
||||
// Instead, we have to use the delete operator
|
||||
// $state.conversations[id] = undefined;
|
||||
},
|
||||
|
||||
toggleAgentTypingStatus($state, { status }) {
|
||||
$state.uiFlags.isAgentTyping = status === 'on';
|
||||
},
|
||||
|
||||
setMetaUserLastSeenAt($state, lastSeen) {
|
||||
$state.meta.userLastSeenAt = lastSeen;
|
||||
},
|
||||
|
||||
setLastMessageId($state) {
|
||||
const { conversations } = $state;
|
||||
const lastMessage = Object.values(conversations).pop();
|
||||
if (!lastMessage) return;
|
||||
const { id } = lastMessage;
|
||||
$state.lastMessageId = id;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
SET_CONVERSATION_ATTRIBUTES,
|
||||
UPDATE_CONVERSATION_ATTRIBUTES,
|
||||
CLEAR_CONVERSATION_ATTRIBUTES,
|
||||
} from '../types';
|
||||
import { getConversationAPI } from '../../api/conversation';
|
||||
|
||||
const state = {
|
||||
id: '',
|
||||
status: '',
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
getConversationParams: $state => $state,
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
getAttributes: async ({ commit }) => {
|
||||
try {
|
||||
const { data } = await getConversationAPI();
|
||||
const { contact_last_seen_at: lastSeen } = data;
|
||||
commit(SET_CONVERSATION_ATTRIBUTES, data);
|
||||
commit('conversation/setMetaUserLastSeenAt', lastSeen, { root: true });
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
}
|
||||
},
|
||||
update({ commit }, data) {
|
||||
commit(UPDATE_CONVERSATION_ATTRIBUTES, data);
|
||||
},
|
||||
clearConversationAttributes: ({ commit }) => {
|
||||
commit('CLEAR_CONVERSATION_ATTRIBUTES');
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
[SET_CONVERSATION_ATTRIBUTES]($state, data) {
|
||||
$state.id = data.id;
|
||||
$state.status = data.status;
|
||||
},
|
||||
[UPDATE_CONVERSATION_ATTRIBUTES]($state, data) {
|
||||
if (data.id === $state.id) {
|
||||
$state.id = data.id;
|
||||
$state.status = data.status;
|
||||
}
|
||||
},
|
||||
[CLEAR_CONVERSATION_ATTRIBUTES]($state) {
|
||||
$state.id = '';
|
||||
$state.status = '';
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import conversationLabels from '../../api/conversationLabels';
|
||||
|
||||
const state = {};
|
||||
|
||||
export const getters = {};
|
||||
|
||||
export const actions = {
|
||||
create: async (_, label) => {
|
||||
try {
|
||||
await conversationLabels.create(label);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
}
|
||||
},
|
||||
destroy: async (_, label) => {
|
||||
try {
|
||||
await conversationLabels.destroy(label);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import events from 'widget/api/events';
|
||||
|
||||
const actions = {
|
||||
create: async (_, { name }) => {
|
||||
try {
|
||||
await events.create(name);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {},
|
||||
getters: {},
|
||||
actions,
|
||||
mutations: {},
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import MessageAPI from '../../api/message';
|
||||
|
||||
const state = {
|
||||
uiFlags: {
|
||||
isUpdating: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
getUIFlags: $state => $state.uiFlags,
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
update: async (
|
||||
{ commit, dispatch, getters: { getUIFlags: uiFlags } },
|
||||
{ email, messageId, submittedValues }
|
||||
) => {
|
||||
if (uiFlags.isUpdating) {
|
||||
return;
|
||||
}
|
||||
commit('toggleUpdateStatus', true);
|
||||
try {
|
||||
await MessageAPI.update({
|
||||
email,
|
||||
messageId,
|
||||
values: submittedValues,
|
||||
});
|
||||
commit(
|
||||
'conversation/updateMessage',
|
||||
{
|
||||
id: messageId,
|
||||
content_attributes: {
|
||||
submitted_email: email,
|
||||
submitted_values: email ? null : submittedValues,
|
||||
},
|
||||
},
|
||||
{ root: true }
|
||||
);
|
||||
dispatch('contacts/get', {}, { root: true });
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
}
|
||||
commit('toggleUpdateStatus', false);
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
toggleUpdateStatus($state, status) {
|
||||
$state.uiFlags.isUpdating = status;
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import { actions } from '../../agent';
|
||||
import { agents } from './data';
|
||||
import { getFromCache, setCache } from 'shared/helpers/cache';
|
||||
import { getAvailableAgents } from 'widget/api/agent';
|
||||
|
||||
let commit = vi.fn();
|
||||
vi.mock('widget/helpers/axios');
|
||||
|
||||
vi.mock('widget/api/agent');
|
||||
vi.mock('shared/helpers/cache');
|
||||
|
||||
describe('#actions', () => {
|
||||
describe('#fetchAvailableAgents', () => {
|
||||
const websiteToken = 'test-token';
|
||||
|
||||
beforeEach(() => {
|
||||
commit = vi.fn();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns cached data if available', async () => {
|
||||
getFromCache.mockReturnValue(agents);
|
||||
await actions.fetchAvailableAgents({ commit }, websiteToken);
|
||||
|
||||
expect(getFromCache).toHaveBeenCalledWith(
|
||||
`chatwoot_available_agents_${websiteToken}`
|
||||
);
|
||||
expect(getAvailableAgents).not.toHaveBeenCalled();
|
||||
expect(setCache).not.toHaveBeenCalled();
|
||||
expect(commit).toHaveBeenCalledWith('setAgents', agents);
|
||||
expect(commit).toHaveBeenCalledWith('setError', false);
|
||||
expect(commit).toHaveBeenCalledWith('setHasFetched', true);
|
||||
});
|
||||
|
||||
it('fetches and caches data if no cache available', async () => {
|
||||
getFromCache.mockReturnValue(null);
|
||||
getAvailableAgents.mockReturnValue({ data: { payload: agents } });
|
||||
|
||||
await actions.fetchAvailableAgents({ commit }, websiteToken);
|
||||
|
||||
expect(getFromCache).toHaveBeenCalledWith(
|
||||
`chatwoot_available_agents_${websiteToken}`
|
||||
);
|
||||
expect(getAvailableAgents).toHaveBeenCalledWith(websiteToken);
|
||||
expect(setCache).toHaveBeenCalledWith(
|
||||
`chatwoot_available_agents_${websiteToken}`,
|
||||
agents
|
||||
);
|
||||
expect(commit).toHaveBeenCalledWith('setAgents', agents);
|
||||
expect(commit).toHaveBeenCalledWith('setError', false);
|
||||
expect(commit).toHaveBeenCalledWith('setHasFetched', true);
|
||||
});
|
||||
|
||||
it('sends correct actions if API is success', async () => {
|
||||
getFromCache.mockReturnValue(null);
|
||||
|
||||
getAvailableAgents.mockReturnValue({ data: { payload: agents } });
|
||||
await actions.fetchAvailableAgents({ commit }, 'Hi');
|
||||
expect(commit.mock.calls).toEqual([
|
||||
['setAgents', agents],
|
||||
['setError', false],
|
||||
['setHasFetched', true],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
getFromCache.mockReturnValue(null);
|
||||
|
||||
getAvailableAgents.mockRejectedValue({
|
||||
message: 'Authentication required',
|
||||
});
|
||||
await actions.fetchAvailableAgents({ commit }, 'Hi');
|
||||
expect(commit.mock.calls).toEqual([
|
||||
['setError', true],
|
||||
['setHasFetched', true],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updatePresence', () => {
|
||||
it('commits the correct presence value', () => {
|
||||
actions.updatePresence({ commit }, { 1: 'online' });
|
||||
expect(commit.mock.calls).toEqual([['updatePresence', { 1: 'online' }]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
export const agents = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'John',
|
||||
avatar_url: '',
|
||||
availability_status: 'online',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Xavier',
|
||||
avatar_url: '',
|
||||
availability_status: 'offline',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Pranav',
|
||||
avatar_url: '',
|
||||
availability_status: 'online',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Nithin',
|
||||
avatar_url: '',
|
||||
availability_status: 'online',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,30 @@
|
||||
import { getters } from '../../agent';
|
||||
import { agents } from './data';
|
||||
|
||||
describe('#getters', () => {
|
||||
it('availableAgents', () => {
|
||||
const state = {
|
||||
records: agents,
|
||||
};
|
||||
expect(getters.availableAgents(state)).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
name: 'John',
|
||||
avatar_url: '',
|
||||
availability_status: 'online',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Pranav',
|
||||
avatar_url: '',
|
||||
availability_status: 'online',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Nithin',
|
||||
avatar_url: '',
|
||||
availability_status: 'online',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { mutations } from '../../agent';
|
||||
import { agents } from './data';
|
||||
|
||||
describe('#mutations', () => {
|
||||
describe('#setAgents', () => {
|
||||
it('set agent records', () => {
|
||||
const state = { records: [] };
|
||||
mutations.setAgents(state, agents);
|
||||
expect(state.records).toEqual(agents);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setError', () => {
|
||||
it('set error flag', () => {
|
||||
const state = { records: [], uiFlags: {} };
|
||||
mutations.setError(state, true);
|
||||
expect(state.uiFlags.isError).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setHasFetched', () => {
|
||||
it('set fetched flag', () => {
|
||||
const state = { records: [], uiFlags: {} };
|
||||
mutations.setHasFetched(state, true);
|
||||
expect(state.uiFlags.hasFetched).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updatePresence', () => {
|
||||
it('updates agent presence', () => {
|
||||
const state = { records: agents };
|
||||
mutations.updatePresence(state, { 1: 'busy', 2: 'online' });
|
||||
expect(state.records).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
name: 'John',
|
||||
avatar_url: '',
|
||||
availability_status: 'busy',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Xavier',
|
||||
avatar_url: '',
|
||||
availability_status: 'online',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Pranav',
|
||||
avatar_url: '',
|
||||
availability_status: 'offline',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Nithin',
|
||||
avatar_url: '',
|
||||
availability_status: 'offline',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { actions } from '../../appConfig';
|
||||
|
||||
const commit = vi.fn();
|
||||
describe('#actions', () => {
|
||||
describe('#setReferrerHost', () => {
|
||||
it('creates actions properly', () => {
|
||||
actions.setReferrerHost({ commit }, 'www.chatwoot.com');
|
||||
expect(commit.mock.calls).toEqual([
|
||||
['SET_REFERRER_HOST', 'www.chatwoot.com'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setBubbleVisibility', () => {
|
||||
it('creates actions properly', () => {
|
||||
actions.setBubbleVisibility({ commit }, false);
|
||||
expect(commit.mock.calls).toEqual([['SET_BUBBLE_VISIBILITY', false]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setWidgetColor', () => {
|
||||
it('creates actions properly', () => {
|
||||
actions.setWidgetColor({ commit }, '#eaeaea');
|
||||
expect(commit.mock.calls).toEqual([['SET_WIDGET_COLOR', '#eaeaea']]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setColorScheme', () => {
|
||||
it('creates actions for dark mode properly', () => {
|
||||
actions.setColorScheme({ commit }, 'dark');
|
||||
expect(commit.mock.calls).toEqual([['SET_COLOR_SCHEME', 'dark']]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setRouteTransitionState', () => {
|
||||
it('creates actions properly', () => {
|
||||
actions.setRouteTransitionState({ commit }, false);
|
||||
expect(commit.mock.calls).toEqual([['SET_ROUTE_UPDATE_STATE', false]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { getters } from '../../appConfig';
|
||||
|
||||
describe('#getters', () => {
|
||||
describe('#getWidgetColor', () => {
|
||||
it('returns correct value', () => {
|
||||
const state = { widgetColor: '#00bcd4' };
|
||||
expect(getters.getWidgetColor(state)).toEqual('#00bcd4');
|
||||
});
|
||||
});
|
||||
describe('#getReferrerHost', () => {
|
||||
it('returns correct value', () => {
|
||||
const state = { referrerHost: 'www.chatwoot.com' };
|
||||
expect(getters.getReferrerHost(state)).toEqual('www.chatwoot.com');
|
||||
});
|
||||
});
|
||||
describe('#getShowUnreadMessagesDialog', () => {
|
||||
it('returns correct value', () => {
|
||||
const state = { showUnreadMessagesDialog: true };
|
||||
expect(getters.getShowUnreadMessagesDialog(state)).toEqual(true);
|
||||
});
|
||||
});
|
||||
describe('#getAvailableMessage', () => {
|
||||
it('returns correct value', () => {
|
||||
const state = { availableMessage: 'We reply quickly' };
|
||||
expect(getters.getAvailableMessage(state)).toEqual('We reply quickly');
|
||||
});
|
||||
});
|
||||
describe('#getWelcomeHeading', () => {
|
||||
it('returns correct value', () => {
|
||||
const state = { welcomeTitle: 'Hello!' };
|
||||
expect(getters.getWelcomeHeading(state)).toEqual('Hello!');
|
||||
});
|
||||
});
|
||||
describe('#getWelcomeTagline', () => {
|
||||
it('returns correct value', () => {
|
||||
const state = { welcomeDescription: 'Welcome to our site' };
|
||||
expect(getters.getWelcomeTagline(state)).toEqual('Welcome to our site');
|
||||
});
|
||||
});
|
||||
describe('#getShouldShowFilePicker', () => {
|
||||
it('returns correct value', () => {
|
||||
const state = { enableFileUpload: true };
|
||||
expect(getters.getShouldShowFilePicker(state)).toEqual(true);
|
||||
});
|
||||
});
|
||||
describe('#getShouldShowEmojiPicker', () => {
|
||||
it('returns correct value', () => {
|
||||
const state = { enableEmojiPicker: true };
|
||||
expect(getters.getShouldShowEmojiPicker(state)).toEqual(true);
|
||||
});
|
||||
});
|
||||
describe('#getCanUserEndConversation', () => {
|
||||
it('returns correct value', () => {
|
||||
const state = { enableEndConversation: true };
|
||||
expect(getters.getCanUserEndConversation(state)).toEqual(true);
|
||||
});
|
||||
});
|
||||
describe('#getUnavailableMessage', () => {
|
||||
it('returns correct value', () => {
|
||||
const state = { unavailableMessage: 'We are offline' };
|
||||
expect(getters.getUnavailableMessage(state)).toEqual('We are offline');
|
||||
});
|
||||
});
|
||||
describe('#getIsUpdatingRoute', () => {
|
||||
it('returns correct value', () => {
|
||||
const state = { isUpdatingRoute: true };
|
||||
expect(getters.getIsUpdatingRoute(state)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { mutations } from '../../appConfig';
|
||||
|
||||
describe('#mutations', () => {
|
||||
describe('#SET_REFERRER_HOST', () => {
|
||||
it('sets referrer host properly', () => {
|
||||
const state = { referrerHost: '' };
|
||||
mutations.SET_REFERRER_HOST(state, 'www.chatwoot.com');
|
||||
expect(state.referrerHost).toEqual('www.chatwoot.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_BUBBLE_VISIBILITY', () => {
|
||||
it('sets bubble visibility properly', () => {
|
||||
const state = { hideMessageBubble: false };
|
||||
mutations.SET_BUBBLE_VISIBILITY(state, true);
|
||||
expect(state.hideMessageBubble).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_WIDGET_COLOR', () => {
|
||||
it('sets widget color properly', () => {
|
||||
const state = { widgetColor: '' };
|
||||
mutations.SET_WIDGET_COLOR(state, '#00bcd4');
|
||||
expect(state.widgetColor).toEqual('#00bcd4');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_COLOR_SCHEME', () => {
|
||||
it('sets dark mode properly', () => {
|
||||
const state = { darkMode: 'light' };
|
||||
mutations.SET_COLOR_SCHEME(state, 'dark');
|
||||
expect(state.darkMode).toEqual('dark');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_ROUTE_UPDATE_STATE', () => {
|
||||
it('sets dark mode properly', () => {
|
||||
const state = { isUpdatingRoute: false };
|
||||
mutations.SET_ROUTE_UPDATE_STATE(state, true);
|
||||
expect(state.isUpdatingRoute).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
import { mutations, actions, getters } from '../articles';
|
||||
import { getMostReadArticles } from 'widget/api/article';
|
||||
import { getFromCache, setCache } from 'shared/helpers/cache';
|
||||
|
||||
vi.mock('widget/api/article');
|
||||
vi.mock('shared/helpers/cache');
|
||||
|
||||
describe('Vuex Articles Module', () => {
|
||||
let state;
|
||||
|
||||
beforeEach(() => {
|
||||
state = {
|
||||
records: [],
|
||||
uiFlags: {
|
||||
isError: false,
|
||||
hasFetched: false,
|
||||
isFetching: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('Mutations', () => {
|
||||
it('sets articles correctly', () => {
|
||||
const articles = [{ id: 1 }, { id: 2 }];
|
||||
mutations.setArticles(state, articles);
|
||||
expect(state.records).toEqual(articles);
|
||||
});
|
||||
|
||||
it('sets error flag correctly', () => {
|
||||
mutations.setError(state, true);
|
||||
expect(state.uiFlags.isError).toBe(true);
|
||||
});
|
||||
|
||||
it('sets fetching state correctly', () => {
|
||||
mutations.setIsFetching(state, true);
|
||||
expect(state.uiFlags.isFetching).toBe(true);
|
||||
});
|
||||
|
||||
it('does not mutate records when no articles are provided', () => {
|
||||
const previousState = { ...state };
|
||||
mutations.setArticles(state, []);
|
||||
expect(state.records).toEqual(previousState.records);
|
||||
});
|
||||
|
||||
it('toggles the error state correctly', () => {
|
||||
mutations.setError(state, true);
|
||||
expect(state.uiFlags.isError).toBe(true);
|
||||
mutations.setError(state, false);
|
||||
expect(state.uiFlags.isError).toBe(false);
|
||||
});
|
||||
|
||||
it('toggles the fetching state correctly', () => {
|
||||
mutations.setIsFetching(state, true);
|
||||
expect(state.uiFlags.isFetching).toBe(true);
|
||||
mutations.setIsFetching(state, false);
|
||||
expect(state.uiFlags.isFetching).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Actions', () => {
|
||||
describe('#fetch', () => {
|
||||
const slug = 'test-slug';
|
||||
const locale = 'en';
|
||||
const articles = [
|
||||
{ id: 1, title: 'Test' },
|
||||
{ id: 2, title: 'Test 2' },
|
||||
];
|
||||
let commit;
|
||||
|
||||
beforeEach(() => {
|
||||
commit = vi.fn();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns cached data if available', async () => {
|
||||
getFromCache.mockReturnValue(articles);
|
||||
|
||||
await actions.fetch({ commit }, { slug, locale });
|
||||
|
||||
expect(getFromCache).toHaveBeenCalledWith(
|
||||
`chatwoot_most_read_articles_${slug}_${locale}`
|
||||
);
|
||||
expect(getMostReadArticles).not.toHaveBeenCalled();
|
||||
expect(setCache).not.toHaveBeenCalled();
|
||||
expect(commit).toHaveBeenCalledWith('setArticles', articles);
|
||||
expect(commit).toHaveBeenCalledWith('setError', false);
|
||||
});
|
||||
|
||||
it('fetches and caches data if no cache available', async () => {
|
||||
getFromCache.mockReturnValue(null);
|
||||
getMostReadArticles.mockReturnValue({ data: { payload: articles } });
|
||||
|
||||
await actions.fetch({ commit }, { slug, locale });
|
||||
|
||||
expect(getFromCache).toHaveBeenCalledWith(
|
||||
`chatwoot_most_read_articles_${slug}_${locale}`
|
||||
);
|
||||
expect(getMostReadArticles).toHaveBeenCalledWith(slug, locale);
|
||||
expect(setCache).toHaveBeenCalledWith(
|
||||
`chatwoot_most_read_articles_${slug}_${locale}`,
|
||||
articles
|
||||
);
|
||||
expect(commit).toHaveBeenCalledWith('setArticles', articles);
|
||||
expect(commit).toHaveBeenCalledWith('setError', false);
|
||||
});
|
||||
|
||||
it('handles API errors correctly', async () => {
|
||||
getFromCache.mockReturnValue(null);
|
||||
getMostReadArticles.mockRejectedValue(new Error('API Error'));
|
||||
|
||||
await actions.fetch({ commit }, { slug, locale });
|
||||
|
||||
expect(commit).toHaveBeenCalledWith('setError', true);
|
||||
expect(commit).toHaveBeenCalledWith('setIsFetching', false);
|
||||
});
|
||||
|
||||
it('does not mutate state when fetching returns an empty payload', async () => {
|
||||
getFromCache.mockReturnValue(null);
|
||||
getMostReadArticles.mockReturnValue({ data: { payload: [] } });
|
||||
|
||||
await actions.fetch({ commit }, { slug, locale });
|
||||
|
||||
expect(commit).toHaveBeenCalledWith('setIsFetching', true);
|
||||
expect(commit).toHaveBeenCalledWith('setError', false);
|
||||
expect(commit).not.toHaveBeenCalledWith(
|
||||
'setArticles',
|
||||
expect.any(Array)
|
||||
);
|
||||
expect(commit).toHaveBeenCalledWith('setIsFetching', false);
|
||||
});
|
||||
|
||||
it('sets loading state during fetch', async () => {
|
||||
getFromCache.mockReturnValue(null);
|
||||
getMostReadArticles.mockReturnValue({ data: { payload: articles } });
|
||||
|
||||
await actions.fetch({ commit }, { slug, locale });
|
||||
|
||||
expect(commit).toHaveBeenCalledWith('setIsFetching', true);
|
||||
expect(commit).toHaveBeenCalledWith('setIsFetching', false);
|
||||
});
|
||||
});
|
||||
|
||||
it('sets error state when fetching fails', async () => {
|
||||
const commit = vi.fn();
|
||||
getMostReadArticles.mockRejectedValueOnce(new Error('Network error'));
|
||||
|
||||
await actions.fetch(
|
||||
{ commit },
|
||||
{ websiteToken: 'token', slug: 'slug', locale: 'en' }
|
||||
);
|
||||
|
||||
expect(commit).toHaveBeenCalledWith('setIsFetching', true);
|
||||
expect(commit).toHaveBeenCalledWith('setError', true);
|
||||
expect(commit).not.toHaveBeenCalledWith('setArticles', expect.any(Array));
|
||||
expect(commit).toHaveBeenCalledWith('setIsFetching', false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Getters', () => {
|
||||
it('returns uiFlags correctly', () => {
|
||||
const result = getters.uiFlags(state);
|
||||
expect(result).toEqual(state.uiFlags);
|
||||
});
|
||||
|
||||
it('returns popularArticles correctly', () => {
|
||||
const result = getters.popularArticles(state);
|
||||
expect(result).toEqual(state.records);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,273 @@
|
||||
import { API } from 'widget/helpers/axios';
|
||||
import { actions } from '../../campaign';
|
||||
import { campaigns } from './data';
|
||||
import { getFromCache, setCache } from 'shared/helpers/cache';
|
||||
|
||||
const commit = vi.fn();
|
||||
const dispatch = vi.fn();
|
||||
vi.mock('widget/helpers/axios');
|
||||
vi.mock('shared/helpers/cache');
|
||||
|
||||
import campaignTimer from 'widget/helpers/campaignTimer';
|
||||
vi.mock('widget/helpers/campaignTimer', () => ({
|
||||
default: {
|
||||
initTimers: vi.fn().mockReturnValue({ mock: true }),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('#actions', () => {
|
||||
describe('#fetchCampaigns', () => {
|
||||
beforeEach(() => {
|
||||
commit.mockClear();
|
||||
getFromCache.mockClear();
|
||||
setCache.mockClear();
|
||||
API.get.mockClear();
|
||||
campaignTimer.initTimers.mockClear();
|
||||
});
|
||||
|
||||
it('uses cached data when available', async () => {
|
||||
getFromCache.mockReturnValue(campaigns);
|
||||
|
||||
await actions.fetchCampaigns(
|
||||
{ commit },
|
||||
{
|
||||
websiteToken: 'XDsafmADasd',
|
||||
currentURL: 'https://chatwoot.com',
|
||||
isInBusinessHours: true,
|
||||
}
|
||||
);
|
||||
|
||||
expect(getFromCache).toHaveBeenCalledWith(
|
||||
'chatwoot_campaigns_XDsafmADasd',
|
||||
60 * 60 * 1000
|
||||
);
|
||||
expect(API.get).not.toHaveBeenCalled();
|
||||
expect(setCache).not.toHaveBeenCalled();
|
||||
expect(commit.mock.calls).toEqual([
|
||||
['setCampaigns', campaigns],
|
||||
['setError', false],
|
||||
]);
|
||||
expect(campaignTimer.initTimers).toHaveBeenCalledWith(
|
||||
{
|
||||
campaigns: [
|
||||
{
|
||||
id: 11,
|
||||
timeOnPage: '20',
|
||||
url: 'https://chatwoot.com',
|
||||
triggerOnlyDuringBusinessHours: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
'XDsafmADasd'
|
||||
);
|
||||
});
|
||||
|
||||
it('fetches and caches data when cache is not available', async () => {
|
||||
getFromCache.mockReturnValue(null);
|
||||
API.get.mockResolvedValue({ data: campaigns });
|
||||
|
||||
await actions.fetchCampaigns(
|
||||
{ commit },
|
||||
{
|
||||
websiteToken: 'XDsafmADasd',
|
||||
currentURL: 'https://chatwoot.com',
|
||||
isInBusinessHours: true,
|
||||
}
|
||||
);
|
||||
|
||||
expect(getFromCache).toHaveBeenCalledWith(
|
||||
'chatwoot_campaigns_XDsafmADasd',
|
||||
60 * 60 * 1000
|
||||
);
|
||||
expect(API.get).toHaveBeenCalled();
|
||||
expect(setCache).toHaveBeenCalledWith(
|
||||
'chatwoot_campaigns_XDsafmADasd',
|
||||
campaigns
|
||||
);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
['setCampaigns', campaigns],
|
||||
['setError', false],
|
||||
]);
|
||||
expect(campaignTimer.initTimers).toHaveBeenCalledWith(
|
||||
{
|
||||
campaigns: [
|
||||
{
|
||||
id: 11,
|
||||
timeOnPage: '20',
|
||||
url: 'https://chatwoot.com',
|
||||
triggerOnlyDuringBusinessHours: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
'XDsafmADasd'
|
||||
);
|
||||
});
|
||||
|
||||
it('sends correct actions if API is error', async () => {
|
||||
getFromCache.mockReturnValue(null);
|
||||
API.get.mockRejectedValue({ message: 'Authentication required' });
|
||||
await actions.fetchCampaigns(
|
||||
{ commit },
|
||||
{
|
||||
websiteToken: 'XDsafmADasd',
|
||||
currentURL: 'https://www.chatwoot.com',
|
||||
isInBusinessHours: true,
|
||||
}
|
||||
);
|
||||
expect(commit.mock.calls).toEqual([['setError', true]]);
|
||||
});
|
||||
});
|
||||
describe('#initCampaigns', () => {
|
||||
const actionParams = {
|
||||
websiteToken: 'XDsafmADasd',
|
||||
currentURL: 'https://chatwoot.com',
|
||||
};
|
||||
it('sends correct actions if campaigns are empty', async () => {
|
||||
await actions.initCampaigns(
|
||||
{
|
||||
dispatch,
|
||||
getters: { getCampaigns: [], getUIFlags: { hasFetched: false } },
|
||||
},
|
||||
actionParams
|
||||
);
|
||||
expect(dispatch.mock.calls).toEqual([['fetchCampaigns', actionParams]]);
|
||||
expect(campaignTimer.initTimers).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('do not refetch if the campaigns are fetched once', async () => {
|
||||
await actions.initCampaigns(
|
||||
{
|
||||
dispatch,
|
||||
getters: { getCampaigns: [], getUIFlags: { hasFetched: true } },
|
||||
},
|
||||
actionParams
|
||||
);
|
||||
expect(dispatch.mock.calls).toEqual([]);
|
||||
expect(campaignTimer.initTimers).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('resets time if campaigns are available', async () => {
|
||||
await actions.initCampaigns(
|
||||
{
|
||||
dispatch,
|
||||
getters: {
|
||||
getCampaigns: campaigns,
|
||||
getUIFlags: { hasFetched: true },
|
||||
},
|
||||
},
|
||||
actionParams
|
||||
);
|
||||
expect(dispatch.mock.calls).toEqual([]);
|
||||
expect(campaignTimer.initTimers).toHaveBeenCalledWith(
|
||||
{
|
||||
campaigns: [
|
||||
{
|
||||
id: 11,
|
||||
timeOnPage: '20',
|
||||
url: 'https://chatwoot.com',
|
||||
triggerOnlyDuringBusinessHours: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
'XDsafmADasd'
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('#startCampaign', () => {
|
||||
it('reset campaign if campaign id is not present in the campaign list', async () => {
|
||||
API.get.mockResolvedValue({ data: campaigns });
|
||||
await actions.startCampaign(
|
||||
{
|
||||
dispatch,
|
||||
getters: { getCampaigns: campaigns },
|
||||
commit,
|
||||
rootState: {
|
||||
appConfig: { isWidgetOpen: true },
|
||||
},
|
||||
},
|
||||
{ campaignId: 32 }
|
||||
);
|
||||
});
|
||||
it('start campaign if campaign id passed', async () => {
|
||||
API.get.mockResolvedValue({ data: campaigns });
|
||||
await actions.startCampaign(
|
||||
{
|
||||
dispatch,
|
||||
getters: { getCampaigns: campaigns },
|
||||
commit,
|
||||
rootState: {
|
||||
appConfig: { isWidgetOpen: false },
|
||||
},
|
||||
},
|
||||
{ campaignId: 1 }
|
||||
);
|
||||
expect(commit.mock.calls).toEqual([['setActiveCampaign', campaigns[0]]]);
|
||||
});
|
||||
});
|
||||
describe('#executeCampaign', () => {
|
||||
it('sends correct actions if execute campaign API is success', async () => {
|
||||
const params = { campaignId: 12, websiteToken: 'XDsafmADasd' };
|
||||
API.post.mockResolvedValue({});
|
||||
await actions.executeCampaign({ commit }, params);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[
|
||||
'conversation/setConversationUIFlag',
|
||||
{
|
||||
isCreating: true,
|
||||
},
|
||||
{
|
||||
root: true,
|
||||
},
|
||||
],
|
||||
['setCampaignExecuted', true],
|
||||
['setActiveCampaign', {}],
|
||||
[
|
||||
'conversation/setConversationUIFlag',
|
||||
{
|
||||
isCreating: false,
|
||||
},
|
||||
{
|
||||
root: true,
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if execute campaign API is failed', async () => {
|
||||
const params = { campaignId: 12, websiteToken: 'XDsafmADasd' };
|
||||
API.post.mockRejectedValue({ message: 'Authentication required' });
|
||||
await actions.executeCampaign({ commit }, params);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[
|
||||
'conversation/setConversationUIFlag',
|
||||
{
|
||||
isCreating: true,
|
||||
},
|
||||
{
|
||||
root: true,
|
||||
},
|
||||
],
|
||||
['setError', true],
|
||||
[
|
||||
'conversation/setConversationUIFlag',
|
||||
{
|
||||
isCreating: false,
|
||||
},
|
||||
{
|
||||
root: true,
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#resetCampaign', () => {
|
||||
it('sends correct actions if execute campaign API is success', async () => {
|
||||
API.post.mockResolvedValue({});
|
||||
await actions.resetCampaign({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
['setCampaignExecuted', false],
|
||||
['setActiveCampaign', {}],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
export const campaigns = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Welcome',
|
||||
description: null,
|
||||
account_id: 1,
|
||||
inbox: {
|
||||
id: 37,
|
||||
channel_id: 1,
|
||||
name: 'Chatwoot',
|
||||
channel_type: 'Channel::WebWidget',
|
||||
},
|
||||
sender: {
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
confirmed: true,
|
||||
email: 'sojan@chatwoot.com',
|
||||
available_name: 'Sojan',
|
||||
id: 10,
|
||||
name: 'Sojan',
|
||||
},
|
||||
message: 'Hey, What brings you today',
|
||||
enabled: true,
|
||||
trigger_rules: {
|
||||
url: 'https://github.com',
|
||||
time_on_page: 10,
|
||||
},
|
||||
created_at: '2021-05-03T04:53:36.354Z',
|
||||
updated_at: '2021-05-03T04:53:36.354Z',
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
title: 'Onboarding Campaign',
|
||||
description: null,
|
||||
account_id: 1,
|
||||
inbox: {
|
||||
id: 37,
|
||||
channel_id: 1,
|
||||
name: 'GitX',
|
||||
channel_type: 'Channel::WebWidget',
|
||||
},
|
||||
sender: {
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
confirmed: true,
|
||||
email: 'sojan@chatwoot.com',
|
||||
available_name: 'Sojan',
|
||||
id: 10,
|
||||
},
|
||||
message: 'Begin your onboarding campaign with a welcome message',
|
||||
enabled: true,
|
||||
trigger_rules: {
|
||||
url: 'https://chatwoot.com',
|
||||
time_on_page: '20',
|
||||
},
|
||||
created_at: '2021-05-03T08:15:35.828Z',
|
||||
updated_at: '2021-05-03T08:15:35.828Z',
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
title: 'Thanks',
|
||||
description: null,
|
||||
account_id: 1,
|
||||
inbox: {
|
||||
id: 37,
|
||||
channel_id: 1,
|
||||
name: 'Chatwoot',
|
||||
channel_type: 'Channel::WebWidget',
|
||||
},
|
||||
sender: {
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
confirmed: true,
|
||||
email: 'nithin@chatwoot.com',
|
||||
available_name: 'Nithin',
|
||||
},
|
||||
message: 'Thanks for coming to the show. How may I help you?',
|
||||
enabled: false,
|
||||
trigger_rules: {
|
||||
url: 'https://noshow.com',
|
||||
time_on_page: 10,
|
||||
},
|
||||
created_at: '2021-05-03T10:22:51.025Z',
|
||||
updated_at: '2021-05-03T10:22:51.025Z',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,133 @@
|
||||
import { getters } from '../../campaign';
|
||||
import { campaigns } from './data';
|
||||
vi.mock('widget/store/index.js', () => ({
|
||||
default: {},
|
||||
}));
|
||||
describe('#getters', () => {
|
||||
it('getCampaigns', () => {
|
||||
const state = {
|
||||
records: campaigns,
|
||||
};
|
||||
expect(getters.getCampaigns(state)).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
title: 'Welcome',
|
||||
description: null,
|
||||
account_id: 1,
|
||||
inbox: {
|
||||
id: 37,
|
||||
channel_id: 1,
|
||||
name: 'Chatwoot',
|
||||
channel_type: 'Channel::WebWidget',
|
||||
},
|
||||
sender: {
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
confirmed: true,
|
||||
email: 'sojan@chatwoot.com',
|
||||
available_name: 'Sojan',
|
||||
id: 10,
|
||||
name: 'Sojan',
|
||||
},
|
||||
message: 'Hey, What brings you today',
|
||||
enabled: true,
|
||||
trigger_rules: {
|
||||
url: 'https://github.com',
|
||||
time_on_page: 10,
|
||||
},
|
||||
created_at: '2021-05-03T04:53:36.354Z',
|
||||
updated_at: '2021-05-03T04:53:36.354Z',
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
title: 'Onboarding Campaign',
|
||||
description: null,
|
||||
account_id: 1,
|
||||
inbox: {
|
||||
id: 37,
|
||||
channel_id: 1,
|
||||
name: 'GitX',
|
||||
channel_type: 'Channel::WebWidget',
|
||||
},
|
||||
sender: {
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
confirmed: true,
|
||||
email: 'sojan@chatwoot.com',
|
||||
available_name: 'Sojan',
|
||||
id: 10,
|
||||
},
|
||||
message: 'Begin your onboarding campaign with a welcome message',
|
||||
enabled: true,
|
||||
trigger_rules: {
|
||||
url: 'https://chatwoot.com',
|
||||
time_on_page: '20',
|
||||
},
|
||||
created_at: '2021-05-03T08:15:35.828Z',
|
||||
updated_at: '2021-05-03T08:15:35.828Z',
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
title: 'Thanks',
|
||||
description: null,
|
||||
account_id: 1,
|
||||
inbox: {
|
||||
id: 37,
|
||||
channel_id: 1,
|
||||
name: 'Chatwoot',
|
||||
channel_type: 'Channel::WebWidget',
|
||||
},
|
||||
sender: {
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
confirmed: true,
|
||||
email: 'nithin@chatwoot.com',
|
||||
available_name: 'Nithin',
|
||||
},
|
||||
message: 'Thanks for coming to the show. How may I help you?',
|
||||
enabled: false,
|
||||
trigger_rules: {
|
||||
url: 'https://noshow.com',
|
||||
time_on_page: 10,
|
||||
},
|
||||
created_at: '2021-05-03T10:22:51.025Z',
|
||||
updated_at: '2021-05-03T10:22:51.025Z',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('getActiveCampaign', () => {
|
||||
const state = {
|
||||
records: campaigns[0],
|
||||
};
|
||||
expect(getters.getCampaigns(state)).toEqual({
|
||||
id: 1,
|
||||
title: 'Welcome',
|
||||
description: null,
|
||||
account_id: 1,
|
||||
inbox: {
|
||||
id: 37,
|
||||
channel_id: 1,
|
||||
name: 'Chatwoot',
|
||||
channel_type: 'Channel::WebWidget',
|
||||
},
|
||||
sender: {
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
confirmed: true,
|
||||
email: 'sojan@chatwoot.com',
|
||||
available_name: 'Sojan',
|
||||
id: 10,
|
||||
name: 'Sojan',
|
||||
},
|
||||
message: 'Hey, What brings you today',
|
||||
enabled: true,
|
||||
trigger_rules: {
|
||||
url: 'https://github.com',
|
||||
time_on_page: 10,
|
||||
},
|
||||
created_at: '2021-05-03T04:53:36.354Z',
|
||||
updated_at: '2021-05-03T04:53:36.354Z',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { mutations } from '../../campaign';
|
||||
import { campaigns } from './data';
|
||||
vi.mock('widget/store/index.js', () => ({
|
||||
default: {},
|
||||
}));
|
||||
|
||||
describe('#mutations', () => {
|
||||
describe('#setCampaigns', () => {
|
||||
it('set campaign records', () => {
|
||||
const state = { records: [], uiFlags: {} };
|
||||
mutations.setCampaigns(state, campaigns);
|
||||
expect(state.records).toEqual(campaigns);
|
||||
expect(state.uiFlags.hasFetched).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setError', () => {
|
||||
it('set error flag', () => {
|
||||
const state = { records: [], uiFlags: {} };
|
||||
mutations.setError(state, true);
|
||||
expect(state.uiFlags.isError).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setActiveCampaign', () => {
|
||||
it('set active campaign', () => {
|
||||
const state = { records: [] };
|
||||
mutations.setActiveCampaign(state, campaigns[0]);
|
||||
expect(state.activeCampaign).toEqual(campaigns[0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setCampaignExecuted', () => {
|
||||
it('set campaign executed flag', () => {
|
||||
const state = { records: [], uiFlags: {}, campaignHasExecuted: false };
|
||||
mutations.setCampaignExecuted(state, true);
|
||||
expect(state.campaignHasExecuted).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import { API } from 'widget/helpers/axios';
|
||||
import { sendMessage } from 'widget/helpers/utils';
|
||||
import { actions } from '../../contacts';
|
||||
|
||||
const commit = vi.fn();
|
||||
const dispatch = vi.fn();
|
||||
|
||||
vi.mock('widget/helpers/axios');
|
||||
vi.mock('widget/helpers/utils', () => ({
|
||||
sendMessage: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('#actions', () => {
|
||||
describe('#setUser', () => {
|
||||
it('sends correct actions if contact object is refreshed ', async () => {
|
||||
const user = {
|
||||
email: 'thoma@sphadikam.com',
|
||||
name: 'Adu Thoma',
|
||||
avatar_url: '',
|
||||
};
|
||||
vi.spyOn(API, 'patch').mockResolvedValue({
|
||||
data: { widget_auth_token: 'token' },
|
||||
});
|
||||
await actions.setUser({ commit, dispatch }, { identifier: 1, user });
|
||||
expect(sendMessage.mock.calls).toEqual([
|
||||
[{ data: { widgetAuthToken: 'token' }, event: 'setAuthCookie' }],
|
||||
]);
|
||||
expect(commit.mock.calls).toEqual([]);
|
||||
expect(dispatch.mock.calls).toEqual([
|
||||
['get'],
|
||||
['conversation/clearConversations', {}, { root: true }],
|
||||
['conversation/fetchOldConversations', {}, { root: true }],
|
||||
['conversationAttributes/getAttributes', {}, { root: true }],
|
||||
]);
|
||||
});
|
||||
|
||||
it('sends correct actions if identifierHash is passed ', async () => {
|
||||
const user = {
|
||||
email: 'thoma@sphadikam.com',
|
||||
name: 'Adu Thoma',
|
||||
avatar_url: '',
|
||||
identifier_hash: '12345',
|
||||
};
|
||||
vi.spyOn(API, 'patch').mockResolvedValue({ data: { id: 1 } });
|
||||
await actions.setUser({ commit, dispatch }, { identifier: 1, user });
|
||||
expect(sendMessage.mock.calls).toEqual([]);
|
||||
expect(commit.mock.calls).toEqual([]);
|
||||
expect(dispatch.mock.calls).toEqual([
|
||||
['get'],
|
||||
['conversation/clearConversations', {}, { root: true }],
|
||||
['conversation/fetchOldConversations', {}, { root: true }],
|
||||
['conversationAttributes/getAttributes', {}, { root: true }],
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not call sendMessage if contact object is not refreshed ', async () => {
|
||||
const user = {
|
||||
email: 'thoma@sphadikam.com',
|
||||
name: 'Adu Thoma',
|
||||
avatar_url: '',
|
||||
};
|
||||
API.patch.mockResolvedValue({ data: { id: 1 } });
|
||||
await actions.setUser({ commit, dispatch }, { identifier: 1, user });
|
||||
expect(sendMessage.mock.calls).toEqual([]);
|
||||
expect(commit.mock.calls).toEqual([]);
|
||||
expect(dispatch.mock.calls).toEqual([['get']]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#update', () => {
|
||||
it('sends correct actions', async () => {
|
||||
const user = {
|
||||
email: 'thoma@sphadikam.com',
|
||||
name: 'Adu Thoma',
|
||||
avatar_url: '',
|
||||
identifier_hash: 'random_hex_identifier_hash',
|
||||
};
|
||||
API.patch.mockResolvedValue({ data: { id: 1 } });
|
||||
await actions.update({ commit, dispatch }, { identifier: 1, user });
|
||||
expect(commit.mock.calls).toEqual([]);
|
||||
expect(dispatch.mock.calls).toEqual([['get']]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { getters } from '../../contacts';
|
||||
|
||||
describe('#getters', () => {
|
||||
it('getCurrentUser', () => {
|
||||
const user = {
|
||||
has_email: true,
|
||||
has_name: true,
|
||||
avatar_url: '',
|
||||
identifier_hash: 'malana_hash',
|
||||
};
|
||||
const state = {
|
||||
currentUser: user,
|
||||
};
|
||||
expect(getters.getCurrentUser(state)).toEqual({
|
||||
has_email: true,
|
||||
has_name: true,
|
||||
avatar_url: '',
|
||||
identifier_hash: 'malana_hash',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { mutations } from '../../contacts';
|
||||
|
||||
describe('#mutations', () => {
|
||||
describe('#SET_CURRENT_USER', () => {
|
||||
it('set current user', () => {
|
||||
const user = {
|
||||
has_email: true,
|
||||
has_name: true,
|
||||
avatar_url: '',
|
||||
identifier_hash: 'malana_hash',
|
||||
};
|
||||
const state = { currentUser: {} };
|
||||
mutations.SET_CURRENT_USER(state, user);
|
||||
expect(state.currentUser).toEqual(user);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,434 @@
|
||||
import { actions } from '../../conversation/actions';
|
||||
import getUuid from '../../../../helpers/uuid';
|
||||
import { API } from 'widget/helpers/axios';
|
||||
|
||||
vi.mock('../../../../helpers/uuid');
|
||||
vi.mock('widget/helpers/axios');
|
||||
|
||||
const commit = vi.fn();
|
||||
const dispatch = vi.fn();
|
||||
|
||||
describe('#actions', () => {
|
||||
describe('#createConversation', () => {
|
||||
it('sends correct mutations', async () => {
|
||||
API.post.mockResolvedValue({
|
||||
data: {
|
||||
contact: { name: 'contact-name' },
|
||||
messages: [{ id: 1, content: 'This is a test message' }],
|
||||
},
|
||||
});
|
||||
|
||||
let windowSpy = vi.spyOn(window, 'window', 'get');
|
||||
windowSpy.mockImplementation(() => ({
|
||||
WOOT_WIDGET: {
|
||||
$root: {
|
||||
$i18n: {
|
||||
locale: 'el',
|
||||
},
|
||||
},
|
||||
},
|
||||
location: {
|
||||
search: '?param=1',
|
||||
},
|
||||
}));
|
||||
await actions.createConversation(
|
||||
{ commit },
|
||||
{ contact: {}, message: 'This is a test message' }
|
||||
);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
['setConversationUIFlag', { isCreating: true }],
|
||||
[
|
||||
'pushMessageToConversation',
|
||||
{ id: 1, content: 'This is a test message' },
|
||||
],
|
||||
['setConversationUIFlag', { isCreating: false }],
|
||||
]);
|
||||
windowSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#addOrUpdateMessage', () => {
|
||||
it('sends correct actions for non-deleted message', () => {
|
||||
actions.addOrUpdateMessage(
|
||||
{ commit },
|
||||
{
|
||||
id: 1,
|
||||
content: 'Hey',
|
||||
content_attributes: {},
|
||||
}
|
||||
);
|
||||
expect(commit).toBeCalledWith('pushMessageToConversation', {
|
||||
id: 1,
|
||||
content: 'Hey',
|
||||
content_attributes: {},
|
||||
});
|
||||
});
|
||||
it('sends correct actions for non-deleted message', () => {
|
||||
actions.addOrUpdateMessage(
|
||||
{ commit },
|
||||
{
|
||||
id: 1,
|
||||
content: 'Hey',
|
||||
content_attributes: { deleted: true },
|
||||
}
|
||||
);
|
||||
expect(commit).toBeCalledWith('deleteMessage', 1);
|
||||
});
|
||||
|
||||
it('plays audio when agent sends a message', () => {
|
||||
actions.addOrUpdateMessage({ commit }, { id: 1, message_type: 1 });
|
||||
expect(commit).toBeCalledWith('pushMessageToConversation', {
|
||||
id: 1,
|
||||
message_type: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#toggleAgentTyping', () => {
|
||||
it('sends correct mutations', () => {
|
||||
actions.toggleAgentTyping({ commit }, { status: true });
|
||||
expect(commit).toBeCalledWith('toggleAgentTypingStatus', {
|
||||
status: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#sendMessage', () => {
|
||||
it('sends correct mutations', async () => {
|
||||
const mockDate = new Date(1466424490000);
|
||||
getUuid.mockImplementationOnce(() => '1111');
|
||||
const spy = vi.spyOn(global, 'Date').mockImplementation(() => mockDate);
|
||||
const windowSpy = vi.spyOn(window, 'window', 'get');
|
||||
windowSpy.mockImplementation(() => ({
|
||||
WOOT_WIDGET: {
|
||||
$root: {
|
||||
$i18n: {
|
||||
locale: 'ar',
|
||||
},
|
||||
},
|
||||
},
|
||||
location: {
|
||||
search: '?param=1',
|
||||
},
|
||||
}));
|
||||
await actions.sendMessage(
|
||||
{ commit, dispatch },
|
||||
{ content: 'hello', replyTo: 124 }
|
||||
);
|
||||
spy.mockRestore();
|
||||
windowSpy.mockRestore();
|
||||
expect(dispatch).toBeCalledWith('sendMessageWithData', {
|
||||
attachments: undefined,
|
||||
content: 'hello',
|
||||
created_at: 1466424490,
|
||||
id: '1111',
|
||||
message_type: 0,
|
||||
replyTo: 124,
|
||||
status: 'in_progress',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#sendAttachment', () => {
|
||||
it('sends correct mutations', () => {
|
||||
const mockDate = new Date(1466424490000);
|
||||
getUuid.mockImplementationOnce(() => '1111');
|
||||
const spy = vi.spyOn(global, 'Date').mockImplementation(() => mockDate);
|
||||
const thumbUrl = '';
|
||||
const attachment = { thumbUrl, fileType: 'file' };
|
||||
|
||||
actions.sendAttachment(
|
||||
{ commit, dispatch },
|
||||
{ attachment, replyTo: 135 }
|
||||
);
|
||||
spy.mockRestore();
|
||||
expect(commit).toBeCalledWith('pushMessageToConversation', {
|
||||
id: '1111',
|
||||
content: undefined,
|
||||
status: 'in_progress',
|
||||
created_at: 1466424490,
|
||||
message_type: 0,
|
||||
replyTo: 135,
|
||||
attachments: [
|
||||
{
|
||||
thumb_url: '',
|
||||
data_url: '',
|
||||
file_type: 'file',
|
||||
status: 'in_progress',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setUserLastSeen', () => {
|
||||
it('sends correct mutations', async () => {
|
||||
API.post.mockResolvedValue({ data: { success: true } });
|
||||
await actions.setUserLastSeen({
|
||||
commit,
|
||||
getters: { getConversationSize: 2 },
|
||||
});
|
||||
expect(commit.mock.calls[0][0]).toEqual('setMetaUserLastSeenAt');
|
||||
});
|
||||
it('sends correct mutations', async () => {
|
||||
API.post.mockResolvedValue({ data: { success: true } });
|
||||
await actions.setUserLastSeen({
|
||||
commit,
|
||||
getters: { getConversationSize: 0 },
|
||||
});
|
||||
expect(commit.mock.calls).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#clearConversations', () => {
|
||||
it('sends correct mutations', () => {
|
||||
actions.clearConversations({ commit });
|
||||
expect(commit).toBeCalledWith('clearConversations');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#fetchOldConversations', () => {
|
||||
it('sends correct actions', async () => {
|
||||
API.get.mockResolvedValue({
|
||||
data: {
|
||||
payload: [
|
||||
{
|
||||
id: 1,
|
||||
text: 'hey',
|
||||
content_attributes: {},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
text: 'welcome',
|
||||
content_attributes: { deleted: true },
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
contact_last_seen_at: 1466424490,
|
||||
},
|
||||
},
|
||||
});
|
||||
await actions.fetchOldConversations({ commit }, {});
|
||||
expect(commit.mock.calls).toEqual([
|
||||
['setConversationListLoading', true],
|
||||
['conversation/setMetaUserLastSeenAt', 1466424490, { root: true }],
|
||||
[
|
||||
'setMessagesInConversation',
|
||||
[
|
||||
{
|
||||
id: 1,
|
||||
text: 'hey',
|
||||
content_attributes: {},
|
||||
},
|
||||
],
|
||||
],
|
||||
['setConversationListLoading', false],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#syncLatestMessages', () => {
|
||||
it('latest message should append to end of list', async () => {
|
||||
const state = {
|
||||
uiFlags: { allMessagesLoaded: false },
|
||||
conversations: {
|
||||
454: {
|
||||
id: 454,
|
||||
content: 'hi',
|
||||
message_type: 0,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
created_at: 1682244355, // Sunday, 23 April 2023 10:05:55
|
||||
conversation_id: 20,
|
||||
},
|
||||
463: {
|
||||
id: 463,
|
||||
content: 'ss',
|
||||
message_type: 0,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
created_at: 1682490729, // Wednesday, 26 April 2023 06:32:09
|
||||
conversation_id: 20,
|
||||
},
|
||||
},
|
||||
lastMessageId: 463,
|
||||
};
|
||||
API.get.mockResolvedValue({
|
||||
data: {
|
||||
payload: [
|
||||
{
|
||||
id: 465,
|
||||
content: 'hi',
|
||||
message_type: 0,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
created_at: 1682504326, // Wednesday, 26 April 2023 10:18:46
|
||||
conversation_id: 20,
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
contact_last_seen_at: 1466424490,
|
||||
},
|
||||
},
|
||||
});
|
||||
await actions.syncLatestMessages({ state, commit }, {});
|
||||
expect(commit.mock.calls).toEqual([
|
||||
['conversation/setMetaUserLastSeenAt', 1466424490, { root: true }],
|
||||
[
|
||||
'setMissingMessagesInConversation',
|
||||
|
||||
{
|
||||
454: {
|
||||
id: 454,
|
||||
content: 'hi',
|
||||
message_type: 0,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
created_at: 1682244355,
|
||||
conversation_id: 20,
|
||||
},
|
||||
463: {
|
||||
id: 463,
|
||||
content: 'ss',
|
||||
message_type: 0,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
created_at: 1682490729,
|
||||
conversation_id: 20,
|
||||
},
|
||||
465: {
|
||||
id: 465,
|
||||
content: 'hi',
|
||||
message_type: 0,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
created_at: 1682504326,
|
||||
conversation_id: 20,
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('old message should insert to exact position', async () => {
|
||||
const state = {
|
||||
uiFlags: { allMessagesLoaded: false },
|
||||
conversations: {
|
||||
454: {
|
||||
id: 454,
|
||||
content: 'hi',
|
||||
message_type: 0,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
created_at: 1682244355, // Sunday, 23 April 2023 10:05:55
|
||||
conversation_id: 20,
|
||||
},
|
||||
463: {
|
||||
id: 463,
|
||||
content: 'ss',
|
||||
message_type: 0,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
created_at: 1682490729, // Wednesday, 26 April 2023 06:32:09
|
||||
conversation_id: 20,
|
||||
},
|
||||
},
|
||||
lastMessageId: 463,
|
||||
};
|
||||
API.get.mockResolvedValue({
|
||||
data: {
|
||||
payload: [
|
||||
{
|
||||
id: 460,
|
||||
content: 'Hi how are you',
|
||||
message_type: 0,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
created_at: 1682417926, // Tuesday, 25 April 2023 10:18:46
|
||||
conversation_id: 20,
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
contact_last_seen_at: 14664223490,
|
||||
},
|
||||
},
|
||||
});
|
||||
await actions.syncLatestMessages({ state, commit }, {});
|
||||
|
||||
expect(commit.mock.calls).toEqual([
|
||||
['conversation/setMetaUserLastSeenAt', 14664223490, { root: true }],
|
||||
[
|
||||
'setMissingMessagesInConversation',
|
||||
|
||||
{
|
||||
454: {
|
||||
id: 454,
|
||||
content: 'hi',
|
||||
message_type: 0,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
created_at: 1682244355,
|
||||
conversation_id: 20,
|
||||
},
|
||||
460: {
|
||||
id: 460,
|
||||
content: 'Hi how are you',
|
||||
message_type: 0,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
created_at: 1682417926,
|
||||
conversation_id: 20,
|
||||
},
|
||||
463: {
|
||||
id: 463,
|
||||
content: 'ss',
|
||||
message_type: 0,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
created_at: 1682490729,
|
||||
conversation_id: 20,
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('abort syncing if there is no missing messages ', async () => {
|
||||
const state = {
|
||||
uiFlags: { allMessagesLoaded: false },
|
||||
conversation: {
|
||||
454: {
|
||||
id: 454,
|
||||
content: 'hi',
|
||||
message_type: 0,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
created_at: 1682244355, // Sunday, 23 April 2023 10:05:55
|
||||
conversation_id: 20,
|
||||
},
|
||||
463: {
|
||||
id: 463,
|
||||
content: 'ss',
|
||||
message_type: 0,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
created_at: 1682490729, // Wednesday, 26 April 2023 06:32:09
|
||||
conversation_id: 20,
|
||||
},
|
||||
},
|
||||
lastMessageId: 463,
|
||||
};
|
||||
API.get.mockResolvedValue({
|
||||
data: {
|
||||
payload: [],
|
||||
meta: {
|
||||
contact_last_seen_at: 14664223490,
|
||||
},
|
||||
},
|
||||
});
|
||||
await actions.syncLatestMessages({ state, commit }, {});
|
||||
|
||||
expect(commit.mock.calls).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,477 @@
|
||||
import { getters } from '../../conversation/getters';
|
||||
|
||||
describe('#getters', () => {
|
||||
it('getConversation', () => {
|
||||
const state = {
|
||||
conversations: {
|
||||
1: {
|
||||
content: 'hello',
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(getters.getConversation(state)).toEqual({
|
||||
1: {
|
||||
content: 'hello',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('getIsCreating', () => {
|
||||
const state = { uiFlags: { isCreating: true } };
|
||||
expect(getters.getIsCreating(state)).toEqual(true);
|
||||
});
|
||||
|
||||
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,
|
||||
isAgentTyping: false,
|
||||
},
|
||||
};
|
||||
expect(getters.getAllMessagesLoaded(state)).toEqual(false);
|
||||
expect(getters.getIsFetchingList(state)).toEqual(false);
|
||||
expect(getters.getIsAgentTyping(state)).toEqual(false);
|
||||
});
|
||||
|
||||
it('getGroupedConversation', () => {
|
||||
expect(
|
||||
getters.getGroupedConversation({
|
||||
conversations: {
|
||||
1: {
|
||||
id: 1,
|
||||
content: 'Thanks for the help',
|
||||
created_at: 1574075964,
|
||||
message_type: 0,
|
||||
},
|
||||
2: {
|
||||
id: 2,
|
||||
content: 'Yes, It makes sense',
|
||||
created_at: 1574092218,
|
||||
message_type: 0,
|
||||
},
|
||||
3: {
|
||||
id: 3,
|
||||
content: 'Hey',
|
||||
created_at: 1574092218,
|
||||
message_type: 1,
|
||||
},
|
||||
4: {
|
||||
id: 4,
|
||||
content: 'Hey',
|
||||
created_at: 1576340623,
|
||||
},
|
||||
5: {
|
||||
id: 5,
|
||||
content: 'How may I help you',
|
||||
created_at: 1576340626,
|
||||
},
|
||||
},
|
||||
})
|
||||
).toEqual([
|
||||
{
|
||||
date: 'Nov 18, 2019',
|
||||
messages: [
|
||||
{
|
||||
id: 1,
|
||||
content: 'Thanks for the help',
|
||||
created_at: 1574075964,
|
||||
showAvatar: false,
|
||||
message_type: 0,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
content: 'Yes, It makes sense',
|
||||
created_at: 1574092218,
|
||||
showAvatar: true,
|
||||
message_type: 0,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
content: 'Hey',
|
||||
created_at: 1574092218,
|
||||
showAvatar: true,
|
||||
message_type: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
date: 'Dec 14, 2019',
|
||||
messages: [
|
||||
{
|
||||
id: 4,
|
||||
content: 'Hey',
|
||||
created_at: 1576340623,
|
||||
showAvatar: false,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
content: 'How may I help you',
|
||||
created_at: 1576340626,
|
||||
showAvatar: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(
|
||||
getters.getGroupedConversation({
|
||||
conversations: {
|
||||
1: {
|
||||
id: 1,
|
||||
content: 'Thanks for the help',
|
||||
created_at: 1574075964,
|
||||
message_type: 0,
|
||||
},
|
||||
2: {
|
||||
id: 2,
|
||||
content: 'Yes, It makes sense',
|
||||
created_at: 1574092218,
|
||||
message_type: 0,
|
||||
},
|
||||
3: {
|
||||
id: 3,
|
||||
content: 'Hey',
|
||||
created_at: 1574092218,
|
||||
message_type: 1,
|
||||
},
|
||||
4: {
|
||||
id: 4,
|
||||
content: 'Hey',
|
||||
created_at: 1576340623,
|
||||
},
|
||||
5: {
|
||||
id: 5,
|
||||
content: 'How may I help you',
|
||||
created_at: 1576340626,
|
||||
message_type: 2,
|
||||
content_type: 'form',
|
||||
content_attributes: {
|
||||
submitted_values: [{ name: 'text', value: 'sample text' }],
|
||||
},
|
||||
},
|
||||
6: {
|
||||
id: 6,
|
||||
content: 'How may I help you',
|
||||
created_at: 1576340626,
|
||||
message_type: 2,
|
||||
content_type: 'form',
|
||||
},
|
||||
7: {
|
||||
id: 7,
|
||||
content: 'How may I help you',
|
||||
created_at: 1576340626,
|
||||
message_type: 2,
|
||||
content_type: 'form',
|
||||
content_attributes: {
|
||||
submitted_values: [{ name: 'text', value: 'sample text' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
).toEqual([
|
||||
{
|
||||
date: 'Nov 18, 2019',
|
||||
messages: [
|
||||
{
|
||||
id: 1,
|
||||
content: 'Thanks for the help',
|
||||
created_at: 1574075964,
|
||||
showAvatar: false,
|
||||
message_type: 0,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
content: 'Yes, It makes sense',
|
||||
created_at: 1574092218,
|
||||
showAvatar: true,
|
||||
message_type: 0,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
content: 'Hey',
|
||||
created_at: 1574092218,
|
||||
showAvatar: true,
|
||||
message_type: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
date: 'Dec 14, 2019',
|
||||
messages: [
|
||||
{
|
||||
id: 4,
|
||||
content: 'Hey',
|
||||
created_at: 1576340623,
|
||||
showAvatar: true,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
content: 'How may I help you',
|
||||
created_at: 1576340626,
|
||||
message_type: 2,
|
||||
content_type: 'form',
|
||||
content_attributes: {
|
||||
submitted_values: [{ name: 'text', value: 'sample text' }],
|
||||
},
|
||||
showAvatar: false,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
content: 'How may I help you',
|
||||
created_at: 1576340626,
|
||||
message_type: 2,
|
||||
content_type: 'form',
|
||||
showAvatar: true,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
content: 'How may I help you',
|
||||
created_at: 1576340626,
|
||||
message_type: 2,
|
||||
content_type: 'form',
|
||||
content_attributes: {
|
||||
submitted_values: [{ name: 'text', value: 'sample text' }],
|
||||
},
|
||||
|
||||
showAvatar: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('getUnreadMessageCount returns', () => {
|
||||
it('0 if there are no messages and last seen is undefined', () => {
|
||||
const state = {
|
||||
conversations: {},
|
||||
meta: {
|
||||
userLastSeenAt: undefined,
|
||||
},
|
||||
};
|
||||
expect(getters.getUnreadMessageCount(state)).toEqual(0);
|
||||
});
|
||||
|
||||
it('0 if there are no messages and last seen is present', () => {
|
||||
const state = {
|
||||
conversations: {},
|
||||
meta: {
|
||||
userLastSeenAt: Date.now(),
|
||||
},
|
||||
};
|
||||
expect(getters.getUnreadMessageCount(state)).toEqual(0);
|
||||
});
|
||||
|
||||
it('unread count if there are messages and last seen is before messages created-at', () => {
|
||||
const state = {
|
||||
conversations: {
|
||||
1: {
|
||||
id: 1,
|
||||
content: 'Thanks for the help',
|
||||
created_at: 1574075964,
|
||||
message_type: 1,
|
||||
},
|
||||
2: {
|
||||
id: 2,
|
||||
content: 'Yes, It makes sense',
|
||||
created_at: 1574092218,
|
||||
message_type: 1,
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
userLastSeenAt: 1474075964,
|
||||
},
|
||||
};
|
||||
expect(getters.getUnreadMessageCount(state)).toEqual(2);
|
||||
});
|
||||
|
||||
it('unread count if there are messages and last seen is after messages created-at', () => {
|
||||
const state = {
|
||||
conversations: {
|
||||
1: {
|
||||
id: 1,
|
||||
content: 'Thanks for the help',
|
||||
created_at: 1574075964,
|
||||
message_type: 1,
|
||||
},
|
||||
2: {
|
||||
id: 2,
|
||||
content: 'Yes, It makes sense',
|
||||
created_at: 1574092218,
|
||||
message_type: 1,
|
||||
},
|
||||
3: {
|
||||
id: 3,
|
||||
content: 'Yes, It makes sense',
|
||||
created_at: 1574092218,
|
||||
message_type: 0,
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
userLastSeenAt: 1674075964,
|
||||
},
|
||||
};
|
||||
expect(getters.getUnreadMessageCount(state)).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUnreadTextMessages returns', () => {
|
||||
it('no messages if there are no messages and last seen is undefined', () => {
|
||||
const state = {
|
||||
conversations: {},
|
||||
meta: {
|
||||
userLastSeenAt: undefined,
|
||||
},
|
||||
};
|
||||
expect(
|
||||
getters.getUnreadTextMessages(state, { getUnreadMessageCount: 0 })
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('0 if there are no messages and last seen is present', () => {
|
||||
const state = {
|
||||
conversations: {},
|
||||
meta: {
|
||||
userLastSeenAt: Date.now(),
|
||||
},
|
||||
};
|
||||
expect(
|
||||
getters.getUnreadTextMessages(state, { getUnreadMessageCount: 0 })
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('only unread text messages from agent if there are messages and last seen is before messages created-at', () => {
|
||||
const state = {
|
||||
conversations: {
|
||||
1: {
|
||||
id: 1,
|
||||
content: 'Thanks for the help',
|
||||
created_at: 1574075964,
|
||||
message_type: 1,
|
||||
},
|
||||
2: {
|
||||
id: 2,
|
||||
content: 'Yes, It makes sense',
|
||||
created_at: 1574092218,
|
||||
message_type: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(
|
||||
getters.getUnreadTextMessages(state, { getUnreadMessageCount: 1 })
|
||||
).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
content: 'Thanks for the help',
|
||||
created_at: 1574075964,
|
||||
message_type: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('unread messages omitting seen messages ', () => {
|
||||
const state = {
|
||||
conversations: {
|
||||
1: {
|
||||
id: 1,
|
||||
content: 'Thanks for the help',
|
||||
created_at: 1574075964,
|
||||
message_type: 1,
|
||||
},
|
||||
2: {
|
||||
id: 2,
|
||||
content: 'Yes, It makes sense',
|
||||
created_at: 1674075965,
|
||||
message_type: 1,
|
||||
},
|
||||
3: {
|
||||
id: 3,
|
||||
content: 'Yes, It makes sense',
|
||||
created_at: 1574092218,
|
||||
message_type: 0,
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
userLastSeenAt: 1674075964,
|
||||
},
|
||||
};
|
||||
expect(
|
||||
getters.getUnreadTextMessages(state, { getUnreadMessageCount: 1 })
|
||||
).toEqual([
|
||||
{
|
||||
id: 2,
|
||||
content: 'Yes, It makes sense',
|
||||
created_at: 1674075965,
|
||||
message_type: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('getMessageCount', () => {
|
||||
const state = {
|
||||
conversations: {
|
||||
1: {
|
||||
content: 'hey, how are you?',
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(getters.getMessageCount(state)).toEqual(1);
|
||||
});
|
||||
|
||||
it('getLastMessage', () => {
|
||||
const state = {
|
||||
conversations: {
|
||||
1: {
|
||||
id: 1,
|
||||
content: 'Thanks for the help',
|
||||
created_at: 1574075964,
|
||||
message_type: 1,
|
||||
},
|
||||
2: {
|
||||
id: 2,
|
||||
content: 'Yes, It makes sense',
|
||||
created_at: 1574092218,
|
||||
message_type: 1,
|
||||
},
|
||||
3: {
|
||||
id: 3,
|
||||
content: 'Yes, It makes sense',
|
||||
created_at: 1574092218,
|
||||
message_type: 0,
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
userLastSeenAt: 1674075964,
|
||||
},
|
||||
};
|
||||
expect(getters.getLastMessage(state).id).toEqual(3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
findUndeliveredMessage,
|
||||
createTemporaryMessage,
|
||||
getNonDeletedMessages,
|
||||
} from '../../conversation/helpers';
|
||||
|
||||
describe('#findUndeliveredMessage', () => {
|
||||
it('returns message objects if exist', () => {
|
||||
const conversation = {
|
||||
1: {
|
||||
id: 1,
|
||||
content: 'Hello',
|
||||
status: 'in_progress',
|
||||
},
|
||||
2: {
|
||||
id: 2,
|
||||
content: 'Hello',
|
||||
status: 'sent',
|
||||
},
|
||||
3: {
|
||||
id: 3,
|
||||
content: 'How may I help you',
|
||||
status: 'sent',
|
||||
},
|
||||
};
|
||||
expect(
|
||||
findUndeliveredMessage(conversation, { content: 'Hello' })
|
||||
).toStrictEqual([{ id: 1, content: 'Hello', status: 'in_progress' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#createTemporaryMessage', () => {
|
||||
it('returns message object', () => {
|
||||
const message = createTemporaryMessage({ content: 'hello' });
|
||||
expect(message.content).toBe('hello');
|
||||
expect(message.status).toBe('in_progress');
|
||||
});
|
||||
it('returns message object with reply to', () => {
|
||||
const message = createTemporaryMessage({
|
||||
content: 'hello',
|
||||
replyTo: 124,
|
||||
});
|
||||
expect(message.content).toBe('hello');
|
||||
expect(message.status).toBe('in_progress');
|
||||
expect(message.replyTo).toBe(124);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getNonDeletedMessages', () => {
|
||||
it('returns non-deleted messages', () => {
|
||||
const messages = [
|
||||
{
|
||||
id: 1,
|
||||
content: 'Hello',
|
||||
content_attributes: {},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
content: 'Hey',
|
||||
content_attributes: { deleted: true },
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
content: 'How may I help you',
|
||||
content_attributes: {},
|
||||
},
|
||||
];
|
||||
expect(getNonDeletedMessages({ messages })).toStrictEqual([
|
||||
{
|
||||
id: 1,
|
||||
content: 'Hello',
|
||||
content_attributes: {},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
content: 'How may I help you',
|
||||
content_attributes: {},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,254 @@
|
||||
import { mutations } from '../../conversation/mutations';
|
||||
|
||||
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('#setConversationUIFlag', () => {
|
||||
it('set uiFlags correctly', () => {
|
||||
const state = { uiFlags: { isFetchingList: false } };
|
||||
mutations.setConversationUIFlag(state, { isCreating: true });
|
||||
expect(state.uiFlags).toEqual({
|
||||
isFetchingList: false,
|
||||
isCreating: 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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#toggleAgentTypingStatus', () => {
|
||||
it('sets isAgentTyping flag to true', () => {
|
||||
const state = { uiFlags: { isAgentTyping: false } };
|
||||
mutations.toggleAgentTypingStatus(state, { status: 'on' });
|
||||
expect(state.uiFlags.isAgentTyping).toEqual(true);
|
||||
});
|
||||
|
||||
it('sets isAgentTyping flag to false', () => {
|
||||
const state = { uiFlags: { isAgentTyping: true } };
|
||||
mutations.toggleAgentTypingStatus(state, { status: 'off' });
|
||||
expect(state.uiFlags.isAgentTyping).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateAttachmentMessageStatus', () => {
|
||||
it('Updates status of loading messages if payload is not empty', () => {
|
||||
const state = {
|
||||
conversations: {
|
||||
rand_id_123: {
|
||||
content: '',
|
||||
id: 'rand_id_123',
|
||||
message_type: 0,
|
||||
status: 'in_progress',
|
||||
attachment: {
|
||||
file: '',
|
||||
file_type: 'image',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const message = {
|
||||
id: '1',
|
||||
content: '',
|
||||
status: 'sent',
|
||||
message_type: 0,
|
||||
attachments: [
|
||||
{
|
||||
file: '',
|
||||
file_type: 'image',
|
||||
},
|
||||
],
|
||||
};
|
||||
mutations.updateAttachmentMessageStatus(state, {
|
||||
message,
|
||||
tempId: 'rand_id_123',
|
||||
});
|
||||
|
||||
expect(state.conversations).toEqual({
|
||||
1: {
|
||||
id: '1',
|
||||
content: '',
|
||||
message_type: 0,
|
||||
status: 'sent',
|
||||
attachments: [
|
||||
{
|
||||
file: '',
|
||||
file_type: 'image',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#clearConversations', () => {
|
||||
it('clears the state', () => {
|
||||
const state = { conversations: { 1: { id: 1 } } };
|
||||
mutations.clearConversations(state);
|
||||
expect(state.conversations).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#deleteMessage', () => {
|
||||
it('delete the message from conversation', () => {
|
||||
const state = { conversations: { 1: { id: 1 } } };
|
||||
mutations.deleteMessage(state, 1);
|
||||
expect(state.conversations).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setMissingMessages', () => {
|
||||
it('sets messages if payload is not empty', () => {
|
||||
const state = {
|
||||
uiFlags: { allMessagesLoaded: false },
|
||||
conversations: {
|
||||
454: {
|
||||
id: 454,
|
||||
content: 'hi',
|
||||
message_type: 0,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
created_at: 1682432667,
|
||||
conversation_id: 20,
|
||||
},
|
||||
464: {
|
||||
id: 464,
|
||||
content: 'hey will be back soon',
|
||||
message_type: 3,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
created_at: 1682490729,
|
||||
conversation_id: 20,
|
||||
},
|
||||
},
|
||||
};
|
||||
mutations.setMessagesInConversation(state, [
|
||||
{
|
||||
id: 455,
|
||||
content: 'Hey billowing-grass-423 how are you?',
|
||||
message_type: 3,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
created_at: 1682432667,
|
||||
conversation_id: 20,
|
||||
},
|
||||
]);
|
||||
expect(state.conversations).toEqual({
|
||||
454: {
|
||||
id: 454,
|
||||
content: 'hi',
|
||||
message_type: 0,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
created_at: 1682432667,
|
||||
conversation_id: 20,
|
||||
},
|
||||
455: {
|
||||
id: 455,
|
||||
content: 'Hey billowing-grass-423 how are you?',
|
||||
message_type: 3,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
created_at: 1682432667,
|
||||
conversation_id: 20,
|
||||
},
|
||||
464: {
|
||||
id: 464,
|
||||
content: 'hey will be back soon',
|
||||
message_type: 3,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
created_at: 1682490729,
|
||||
conversation_id: 20,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { actions } from '../../conversationAttributes';
|
||||
import { API } from 'widget/helpers/axios';
|
||||
|
||||
const commit = vi.fn();
|
||||
vi.mock('widget/helpers/axios');
|
||||
|
||||
describe('#actions', () => {
|
||||
describe('#get attributes', () => {
|
||||
it('sends mutation if api is success', async () => {
|
||||
API.get.mockResolvedValue({ data: { id: 1, status: 'pending' } });
|
||||
await actions.getAttributes({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
['SET_CONVERSATION_ATTRIBUTES', { id: 1, status: 'pending' }],
|
||||
['conversation/setMetaUserLastSeenAt', undefined, { root: true }],
|
||||
]);
|
||||
});
|
||||
it('doesnot send mutation if api is error', async () => {
|
||||
API.get.mockRejectedValue({ message: 'Invalid Headers' });
|
||||
await actions.getAttributes({ commit });
|
||||
expect(commit.mock.calls).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#update attributes', () => {
|
||||
it('sends correct mutations', () => {
|
||||
actions.update({ commit }, { id: 1, status: 'pending' });
|
||||
expect(commit).toBeCalledWith('UPDATE_CONVERSATION_ATTRIBUTES', {
|
||||
id: 1,
|
||||
status: 'pending',
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#clear attributes', () => {
|
||||
it('sends correct mutations', () => {
|
||||
actions.clearConversationAttributes({ commit });
|
||||
expect(commit).toBeCalledWith('CLEAR_CONVERSATION_ATTRIBUTES');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { getters } from '../../conversationAttributes';
|
||||
|
||||
describe('#getters', () => {
|
||||
it('getConversationParams', () => {
|
||||
const state = {
|
||||
id: 1,
|
||||
status: 'pending',
|
||||
};
|
||||
expect(getters.getConversationParams(state)).toEqual({
|
||||
id: 1,
|
||||
status: 'pending',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { mutations } from '../../conversationAttributes';
|
||||
|
||||
describe('#mutations', () => {
|
||||
describe('#SET_CONVERSATION_ATTRIBUTES', () => {
|
||||
it('set status of the conversation', () => {
|
||||
const state = { id: '', status: '' };
|
||||
mutations.SET_CONVERSATION_ATTRIBUTES(state, {
|
||||
id: 1,
|
||||
status: 'open',
|
||||
});
|
||||
expect(state).toEqual({ id: 1, status: 'open' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#UPDATE_CONVERSATION_ATTRIBUTES', () => {
|
||||
it('update status if it is same conversation', () => {
|
||||
const state = { id: 1, status: 'pending' };
|
||||
mutations.UPDATE_CONVERSATION_ATTRIBUTES(state, {
|
||||
id: 1,
|
||||
status: 'open',
|
||||
});
|
||||
expect(state).toEqual({ id: 1, status: 'open' });
|
||||
});
|
||||
it('doesnot update status if it is not the same conversation', () => {
|
||||
const state = { id: 1, status: 'pending' };
|
||||
mutations.UPDATE_CONVERSATION_ATTRIBUTES(state, {
|
||||
id: 2,
|
||||
status: 'open',
|
||||
});
|
||||
expect(state).toEqual({ id: 1, status: 'pending' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#CLEAR_CONVERSATION_ATTRIBUTES', () => {
|
||||
it('clear status if it is same conversation', () => {
|
||||
const state = { id: 1, status: 'open' };
|
||||
mutations.CLEAR_CONVERSATION_ATTRIBUTES(state, {
|
||||
id: 1,
|
||||
status: 'open',
|
||||
});
|
||||
expect(state).toEqual({ id: '', status: '' });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { API } from 'widget/helpers/axios';
|
||||
import { actions } from '../../message';
|
||||
|
||||
const commit = vi.fn();
|
||||
vi.mock('widget/helpers/axios');
|
||||
|
||||
describe('#actions', () => {
|
||||
describe('#update', () => {
|
||||
it('sends correct actions', async () => {
|
||||
const user = {
|
||||
email: 'john@acme.inc',
|
||||
messageId: 10,
|
||||
submittedValues: {
|
||||
email: 'john@acme.inc',
|
||||
},
|
||||
};
|
||||
API.patch.mockResolvedValue({
|
||||
data: { contact: { pubsub_token: '8npuMUfDgizrwVoqcK1t7FMY' } },
|
||||
});
|
||||
await actions.update(
|
||||
{
|
||||
commit,
|
||||
getters: {
|
||||
getUIFlags: {
|
||||
isUpdating: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
user
|
||||
);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
['toggleUpdateStatus', true],
|
||||
[
|
||||
'conversation/updateMessage',
|
||||
{
|
||||
id: 10,
|
||||
content_attributes: {
|
||||
submitted_email: 'john@acme.inc',
|
||||
submitted_values: null,
|
||||
},
|
||||
},
|
||||
{ root: true },
|
||||
],
|
||||
['toggleUpdateStatus', false],
|
||||
]);
|
||||
});
|
||||
|
||||
it('blocks all new action calls when isUpdating', async () => {
|
||||
await actions.update(
|
||||
{
|
||||
commit,
|
||||
getters: {
|
||||
getUIFlags: {
|
||||
isUpdating: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
expect(commit.mock.calls).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { getters } from '../../message';
|
||||
|
||||
describe('#getters', () => {
|
||||
it('getUIFlags', () => {
|
||||
const state = {
|
||||
uiFlags: {
|
||||
isUpdating: false,
|
||||
},
|
||||
};
|
||||
expect(getters.getUIFlags(state)).toEqual({
|
||||
isUpdating: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { mutations } from '../../message';
|
||||
|
||||
describe('#mutations', () => {
|
||||
describe('#toggleUpdateStatus', () => {
|
||||
it('set update flags', () => {
|
||||
const state = { uiFlags: { status: '' } };
|
||||
mutations.toggleUpdateStatus(state, 'sent');
|
||||
expect(state.uiFlags.isUpdating).toEqual('sent');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user