Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import { DataManager } from '../../CacheHelper/DataManager';
|
||||
|
||||
describe('DataManager', () => {
|
||||
const accountId = 'test-account';
|
||||
let dataManager;
|
||||
|
||||
beforeEach(async () => {
|
||||
dataManager = new DataManager(accountId);
|
||||
await dataManager.initDb();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
const tx = dataManager.db.transaction(
|
||||
dataManager.modelsToSync,
|
||||
'readwrite'
|
||||
);
|
||||
dataManager.modelsToSync.forEach(modelName => {
|
||||
tx.objectStore(modelName).clear();
|
||||
});
|
||||
await tx.done;
|
||||
});
|
||||
|
||||
describe('initDb', () => {
|
||||
it('should initialize the database', async () => {
|
||||
expect(dataManager.db).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should return the same instance of the database', async () => {
|
||||
const db1 = await dataManager.initDb();
|
||||
const db2 = await dataManager.initDb();
|
||||
expect(db1).toBe(db2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateModel', () => {
|
||||
it('should throw an error for empty input', async () => {
|
||||
expect(() => {
|
||||
dataManager.validateModel();
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('should throw an error for invalid model', async () => {
|
||||
expect(() => {
|
||||
dataManager.validateModel('invalid-model');
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('should not throw an error for valid model', async () => {
|
||||
expect(dataManager.validateModel('label')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('replace', () => {
|
||||
it('should replace existing data in the specified model', async () => {
|
||||
const inboxData = [
|
||||
{ id: 1, name: 'inbox-1' },
|
||||
{ id: 2, name: 'inbox-2' },
|
||||
];
|
||||
const newData = [
|
||||
{ id: 3, name: 'inbox-3' },
|
||||
{ id: 4, name: 'inbox-4' },
|
||||
];
|
||||
|
||||
await dataManager.push({ modelName: 'inbox', data: inboxData });
|
||||
await dataManager.replace({ modelName: 'inbox', data: newData });
|
||||
const result = await dataManager.get({ modelName: 'inbox' });
|
||||
expect(result).toEqual(newData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('push', () => {
|
||||
it('should add data to the specified model', async () => {
|
||||
const inboxData = { id: 1, name: 'inbox-1' };
|
||||
|
||||
await dataManager.push({ modelName: 'inbox', data: inboxData });
|
||||
const result = await dataManager.get({ modelName: 'inbox' });
|
||||
expect(result).toEqual([inboxData]);
|
||||
});
|
||||
|
||||
it('should add multiple items to the specified model if an array of data is provided', async () => {
|
||||
const inboxData = [
|
||||
{ id: 1, name: 'inbox-1' },
|
||||
{ id: 2, name: 'inbox-2' },
|
||||
];
|
||||
|
||||
await dataManager.push({ modelName: 'inbox', data: inboxData });
|
||||
const result = await dataManager.get({ modelName: 'inbox' });
|
||||
expect(result).toEqual(inboxData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should return all data in the specified model', async () => {
|
||||
const inboxData = [
|
||||
{ id: 1, name: 'inbox-1' },
|
||||
{ id: 2, name: 'inbox-2' },
|
||||
];
|
||||
|
||||
await dataManager.push({ modelName: 'inbox', data: inboxData });
|
||||
const result = await dataManager.get({ modelName: 'inbox' });
|
||||
expect(result).toEqual(inboxData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setCacheKeys', () => {
|
||||
it('should add cache keys for each model', async () => {
|
||||
const cacheKeys = { inbox: 'cache-key-1', label: 'cache-key-2' };
|
||||
|
||||
await dataManager.setCacheKeys(cacheKeys);
|
||||
const result = await dataManager.getCacheKey('inbox');
|
||||
expect(result).toEqual(cacheKeys.inbox);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { loadScript } from '../DOMHelpers';
|
||||
import { JSDOM } from 'jsdom';
|
||||
|
||||
describe('loadScript', () => {
|
||||
let dom;
|
||||
let window;
|
||||
let document;
|
||||
|
||||
beforeEach(() => {
|
||||
dom = new JSDOM('<!DOCTYPE html><html><head></head><body></body></html>', {
|
||||
url: 'http://localhost',
|
||||
});
|
||||
window = dom.window;
|
||||
document = window.document;
|
||||
global.document = document;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
delete global.document;
|
||||
});
|
||||
|
||||
it('should load a script successfully', async () => {
|
||||
const src = 'https://example.com/script.js';
|
||||
const loadPromise = loadScript(src, {});
|
||||
|
||||
// Simulate successful script load
|
||||
setTimeout(() => {
|
||||
const script = document.querySelector(`script[src="${src}"]`);
|
||||
if (script) {
|
||||
script.dispatchEvent(new window.Event('load'));
|
||||
}
|
||||
}, 0);
|
||||
|
||||
const script = await loadPromise;
|
||||
|
||||
expect(script).toBeTruthy();
|
||||
expect(script.getAttribute('src')).toBe(src);
|
||||
expect(script.getAttribute('data-loaded')).toBe('true');
|
||||
});
|
||||
|
||||
it('should not load a script if document is not available', async () => {
|
||||
delete global.document;
|
||||
const result = await loadScript('https://example.com/script.js', {});
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should use an existing script if already present', async () => {
|
||||
const src = 'https://example.com/existing-script.js';
|
||||
const existingScript = document.createElement('script');
|
||||
existingScript.src = src;
|
||||
existingScript.setAttribute('data-loaded', 'true');
|
||||
document.head.appendChild(existingScript);
|
||||
|
||||
const script = await loadScript(src, {});
|
||||
|
||||
expect(script).toBe(existingScript);
|
||||
});
|
||||
|
||||
it('should set custom attributes on the script element', async () => {
|
||||
const src = 'https://example.com/custom-script.js';
|
||||
const options = {
|
||||
type: 'module',
|
||||
async: false,
|
||||
defer: true,
|
||||
crossOrigin: 'anonymous',
|
||||
noModule: true,
|
||||
referrerPolicy: 'origin',
|
||||
id: 'custom-script',
|
||||
attrs: { 'data-custom': 'value' },
|
||||
};
|
||||
|
||||
const loadPromise = loadScript(src, options);
|
||||
|
||||
// Simulate successful script load
|
||||
setTimeout(() => {
|
||||
const script = document.querySelector(`script[src="${src}"]`);
|
||||
if (script) {
|
||||
script.dispatchEvent(new window.Event('load'));
|
||||
}
|
||||
}, 0);
|
||||
|
||||
const script = await loadPromise;
|
||||
|
||||
expect(script.type).toBe('module');
|
||||
expect(script.async).toBe(false);
|
||||
expect(script.defer).toBe(true);
|
||||
expect(script.crossOrigin).toBe('anonymous');
|
||||
expect(script.noModule).toBe(true);
|
||||
expect(script.referrerPolicy).toBe('origin');
|
||||
expect(script.id).toBe('custom-script');
|
||||
expect(script.getAttribute('data-custom')).toBe('value');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,349 @@
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { differenceInSeconds } from 'date-fns';
|
||||
import {
|
||||
isAConversationRoute,
|
||||
isAInboxViewRoute,
|
||||
isNotificationRoute,
|
||||
} from 'dashboard/helper/routeHelpers';
|
||||
import ReconnectService from 'dashboard/helper/ReconnectService';
|
||||
|
||||
vi.mock('shared/helpers/mitt', () => ({
|
||||
emitter: {
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
emit: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('date-fns', () => ({
|
||||
differenceInSeconds: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('dashboard/helper/routeHelpers', () => ({
|
||||
isAConversationRoute: vi.fn(),
|
||||
isAInboxViewRoute: vi.fn(),
|
||||
isNotificationRoute: vi.fn(),
|
||||
}));
|
||||
|
||||
const storeMock = {
|
||||
dispatch: vi.fn(),
|
||||
getters: {
|
||||
getAppliedConversationFiltersQuery: [],
|
||||
'customViews/getActiveConversationFolder': { query: {} },
|
||||
'notifications/getNotificationFilters': {},
|
||||
},
|
||||
};
|
||||
|
||||
const routerMock = {
|
||||
currentRoute: {
|
||||
value: {
|
||||
name: '',
|
||||
params: { conversation_id: null },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('ReconnectService', () => {
|
||||
let reconnectService;
|
||||
|
||||
beforeEach(() => {
|
||||
window.addEventListener = vi.fn();
|
||||
window.removeEventListener = vi.fn();
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
value: { reload: vi.fn() },
|
||||
});
|
||||
reconnectService = new ReconnectService(storeMock, routerMock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should initialize with store, router, and setup event listeners', () => {
|
||||
expect(reconnectService.store).toBe(storeMock);
|
||||
expect(reconnectService.router).toBe(routerMock);
|
||||
expect(window.addEventListener).toHaveBeenCalledWith(
|
||||
'online',
|
||||
reconnectService.handleOnlineEvent
|
||||
);
|
||||
expect(emitter.on).toHaveBeenCalledWith(
|
||||
BUS_EVENTS.WEBSOCKET_RECONNECT,
|
||||
reconnectService.onReconnect
|
||||
);
|
||||
expect(emitter.on).toHaveBeenCalledWith(
|
||||
BUS_EVENTS.WEBSOCKET_DISCONNECT,
|
||||
reconnectService.onDisconnect
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('disconnect', () => {
|
||||
it('should remove event listeners', () => {
|
||||
reconnectService.disconnect();
|
||||
expect(window.removeEventListener).toHaveBeenCalledWith(
|
||||
'online',
|
||||
reconnectService.handleOnlineEvent
|
||||
);
|
||||
expect(emitter.off).toHaveBeenCalledWith(
|
||||
BUS_EVENTS.WEBSOCKET_RECONNECT,
|
||||
reconnectService.onReconnect
|
||||
);
|
||||
expect(emitter.off).toHaveBeenCalledWith(
|
||||
BUS_EVENTS.WEBSOCKET_DISCONNECT,
|
||||
reconnectService.onDisconnect
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSecondsSinceDisconnect', () => {
|
||||
it('should return 0 if disconnectTime is null', () => {
|
||||
reconnectService.disconnectTime = null;
|
||||
expect(reconnectService.getSecondsSinceDisconnect()).toBe(0);
|
||||
});
|
||||
|
||||
it('should return the number of seconds + threshold since disconnect', () => {
|
||||
reconnectService.disconnectTime = new Date();
|
||||
differenceInSeconds.mockReturnValue(100);
|
||||
expect(reconnectService.getSecondsSinceDisconnect()).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleOnlineEvent', () => {
|
||||
it('should reload the page if disconnected for more than 3 hours', () => {
|
||||
reconnectService.getSecondsSinceDisconnect = vi
|
||||
.fn()
|
||||
.mockReturnValue(10801);
|
||||
reconnectService.handleOnlineEvent();
|
||||
expect(window.location.reload).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not reload the page if disconnected for less than 3 hours', () => {
|
||||
reconnectService.getSecondsSinceDisconnect = vi
|
||||
.fn()
|
||||
.mockReturnValue(10799);
|
||||
reconnectService.handleOnlineEvent();
|
||||
expect(window.location.reload).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchConversations', () => {
|
||||
it('should update the filters with disconnected time and the threshold', async () => {
|
||||
reconnectService.getSecondsSinceDisconnect = vi.fn().mockReturnValue(100);
|
||||
await reconnectService.fetchConversations();
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith('updateChatListFilters', {
|
||||
page: null,
|
||||
updatedWithin: 115,
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch updateChatListFilters and fetchAllConversations', async () => {
|
||||
reconnectService.getSecondsSinceDisconnect = vi.fn().mockReturnValue(100);
|
||||
await reconnectService.fetchConversations();
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith('updateChatListFilters', {
|
||||
page: null,
|
||||
updatedWithin: 115,
|
||||
});
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith('fetchAllConversations');
|
||||
});
|
||||
|
||||
it('should dispatch updateChatListFilters and reset updatedWithin', async () => {
|
||||
reconnectService.getSecondsSinceDisconnect = vi.fn().mockReturnValue(100);
|
||||
await reconnectService.fetchConversations();
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith('updateChatListFilters', {
|
||||
updatedWithin: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchFilteredOrSavedConversations', () => {
|
||||
it('should dispatch fetchFilteredConversations', async () => {
|
||||
const payload = { test: 'data' };
|
||||
await reconnectService.fetchFilteredOrSavedConversations(payload);
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith(
|
||||
'fetchFilteredConversations',
|
||||
{ queryData: payload, page: 1 }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchConversationsOnReconnect', () => {
|
||||
it('should fetch filtered or saved conversations if query exists', async () => {
|
||||
storeMock.getters.getAppliedConversationFiltersQuery = {
|
||||
payload: [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: ['open'],
|
||||
},
|
||||
],
|
||||
};
|
||||
const spy = vi.spyOn(
|
||||
reconnectService,
|
||||
'fetchFilteredOrSavedConversations'
|
||||
);
|
||||
|
||||
await reconnectService.fetchConversationsOnReconnect();
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
storeMock.getters.getAppliedConversationFiltersQuery
|
||||
);
|
||||
});
|
||||
|
||||
it('should fetch all conversations if no query exists', async () => {
|
||||
storeMock.getters.getAppliedConversationFiltersQuery = [];
|
||||
storeMock.getters['customViews/getActiveConversationFolder'] = {
|
||||
query: null,
|
||||
};
|
||||
|
||||
const spy = vi.spyOn(reconnectService, 'fetchConversations');
|
||||
|
||||
await reconnectService.fetchConversationsOnReconnect();
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fetch filtered or saved conversations if active folder query exists and no applied query', async () => {
|
||||
storeMock.getters.getAppliedConversationFiltersQuery = [];
|
||||
storeMock.getters['customViews/getActiveConversationFolder'] = {
|
||||
query: { test: 'activeFolderQuery' },
|
||||
};
|
||||
|
||||
const spy = vi.spyOn(
|
||||
reconnectService,
|
||||
'fetchFilteredOrSavedConversations'
|
||||
);
|
||||
|
||||
await reconnectService.fetchConversationsOnReconnect();
|
||||
|
||||
expect(spy).toHaveBeenCalledWith({ test: 'activeFolderQuery' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchConversationMessagesOnReconnect', () => {
|
||||
it('should dispatch syncActiveConversationMessages if conversationId exists', async () => {
|
||||
routerMock.currentRoute.value.params.conversation_id = 1;
|
||||
await reconnectService.fetchConversationMessagesOnReconnect();
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith(
|
||||
'syncActiveConversationMessages',
|
||||
{ conversationId: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should not dispatch syncActiveConversationMessages if conversationId does not exist', async () => {
|
||||
routerMock.currentRoute.value.params.conversation_id = null;
|
||||
await reconnectService.fetchConversationMessagesOnReconnect();
|
||||
expect(storeMock.dispatch).not.toHaveBeenCalledWith(
|
||||
'syncActiveConversationMessages',
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchNotificationsOnReconnect', () => {
|
||||
it('should dispatch notifications/index', async () => {
|
||||
const filter = { test: 'filter' };
|
||||
await reconnectService.fetchNotificationsOnReconnect(filter);
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith('notifications/index', {
|
||||
...filter,
|
||||
page: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('revalidateCaches', () => {
|
||||
it('should dispatch revalidate actions for labels, inboxes, and teams', async () => {
|
||||
storeMock.dispatch.mockResolvedValueOnce({
|
||||
label: 'labelKey',
|
||||
inbox: 'inboxKey',
|
||||
team: 'teamKey',
|
||||
});
|
||||
await reconnectService.revalidateCaches();
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith('accounts/getCacheKeys');
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith('labels/revalidate', {
|
||||
newKey: 'labelKey',
|
||||
});
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith('inboxes/revalidate', {
|
||||
newKey: 'inboxKey',
|
||||
});
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith('teams/revalidate', {
|
||||
newKey: 'teamKey',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleRouteSpecificFetch', () => {
|
||||
it('should fetch conversations and messages if current route is a conversation route', async () => {
|
||||
isAConversationRoute.mockReturnValue(true);
|
||||
const spyConversations = vi.spyOn(
|
||||
reconnectService,
|
||||
'fetchConversationsOnReconnect'
|
||||
);
|
||||
const spyMessages = vi.spyOn(
|
||||
reconnectService,
|
||||
'fetchConversationMessagesOnReconnect'
|
||||
);
|
||||
await reconnectService.handleRouteSpecificFetch();
|
||||
expect(spyConversations).toHaveBeenCalled();
|
||||
expect(spyMessages).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fetch notifications if current route is an inbox view route', async () => {
|
||||
isAInboxViewRoute.mockReturnValue(true);
|
||||
const spy = vi.spyOn(reconnectService, 'fetchNotificationsOnReconnect');
|
||||
await reconnectService.handleRouteSpecificFetch();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fetch notifications if current route is a notification route', async () => {
|
||||
isNotificationRoute.mockReturnValue(true);
|
||||
const spy = vi.spyOn(reconnectService, 'fetchNotificationsOnReconnect');
|
||||
await reconnectService.handleRouteSpecificFetch();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setConversationLastMessageId', () => {
|
||||
it('should dispatch setConversationLastMessageId if conversationId exists', async () => {
|
||||
routerMock.currentRoute.value.params.conversation_id = 1;
|
||||
await reconnectService.setConversationLastMessageId();
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith(
|
||||
'setConversationLastMessageId',
|
||||
{ conversationId: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should not dispatch setConversationLastMessageId if conversationId does not exist', async () => {
|
||||
routerMock.currentRoute.value.params.conversation_id = null;
|
||||
await reconnectService.setConversationLastMessageId();
|
||||
expect(storeMock.dispatch).not.toHaveBeenCalledWith(
|
||||
'setConversationLastMessageId',
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onDisconnect', () => {
|
||||
it('should set disconnectTime and call setConversationLastMessageId', () => {
|
||||
reconnectService.setConversationLastMessageId = vi.fn();
|
||||
reconnectService.onDisconnect();
|
||||
expect(reconnectService.disconnectTime).toBeInstanceOf(Date);
|
||||
expect(reconnectService.setConversationLastMessageId).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onReconnect', () => {
|
||||
it('should handle route-specific fetch, revalidate caches, and emit WEBSOCKET_RECONNECT_COMPLETED event', async () => {
|
||||
reconnectService.handleRouteSpecificFetch = vi.fn();
|
||||
reconnectService.revalidateCaches = vi.fn();
|
||||
await reconnectService.onReconnect();
|
||||
expect(reconnectService.handleRouteSpecificFetch).toHaveBeenCalled();
|
||||
expect(reconnectService.revalidateCaches).toHaveBeenCalled();
|
||||
expect(emitter.emit).toHaveBeenCalledWith(
|
||||
BUS_EVENTS.WEBSOCKET_RECONNECT_COMPLETED
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import Timer from '../Timer';
|
||||
|
||||
describe('Timer', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('initializes with elapsed 0 and no interval', () => {
|
||||
const timer = new Timer();
|
||||
expect(timer.elapsed).toBe(0);
|
||||
expect(timer.intervalId).toBeNull();
|
||||
});
|
||||
|
||||
it('accepts an onTick callback', () => {
|
||||
const onTick = vi.fn();
|
||||
const timer = new Timer(onTick);
|
||||
expect(timer.onTick).toBe(onTick);
|
||||
});
|
||||
});
|
||||
|
||||
describe('start', () => {
|
||||
it('starts the timer and increments elapsed every second', () => {
|
||||
const timer = new Timer();
|
||||
timer.start();
|
||||
|
||||
expect(timer.elapsed).toBe(0);
|
||||
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(timer.elapsed).toBe(1);
|
||||
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(timer.elapsed).toBe(2);
|
||||
|
||||
vi.advanceTimersByTime(3000);
|
||||
expect(timer.elapsed).toBe(5);
|
||||
});
|
||||
|
||||
it('calls onTick callback with elapsed value', () => {
|
||||
const onTick = vi.fn();
|
||||
const timer = new Timer(onTick);
|
||||
timer.start();
|
||||
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(onTick).toHaveBeenCalledWith(1);
|
||||
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(onTick).toHaveBeenCalledWith(2);
|
||||
|
||||
expect(onTick).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('resets elapsed to 0 when restarted', () => {
|
||||
const timer = new Timer();
|
||||
timer.start();
|
||||
|
||||
vi.advanceTimersByTime(5000);
|
||||
expect(timer.elapsed).toBe(5);
|
||||
|
||||
timer.start();
|
||||
expect(timer.elapsed).toBe(0);
|
||||
|
||||
vi.advanceTimersByTime(2000);
|
||||
expect(timer.elapsed).toBe(2);
|
||||
});
|
||||
|
||||
it('clears previous interval when restarted', () => {
|
||||
const timer = new Timer();
|
||||
timer.start();
|
||||
const firstIntervalId = timer.intervalId;
|
||||
|
||||
timer.start();
|
||||
expect(timer.intervalId).not.toBe(firstIntervalId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stop', () => {
|
||||
it('stops the timer and resets elapsed to 0', () => {
|
||||
const timer = new Timer();
|
||||
timer.start();
|
||||
|
||||
vi.advanceTimersByTime(3000);
|
||||
expect(timer.elapsed).toBe(3);
|
||||
|
||||
timer.stop();
|
||||
expect(timer.elapsed).toBe(0);
|
||||
expect(timer.intervalId).toBeNull();
|
||||
});
|
||||
|
||||
it('prevents further increments after stopping', () => {
|
||||
const timer = new Timer();
|
||||
timer.start();
|
||||
|
||||
vi.advanceTimersByTime(2000);
|
||||
timer.stop();
|
||||
|
||||
vi.advanceTimersByTime(5000);
|
||||
expect(timer.elapsed).toBe(0);
|
||||
});
|
||||
|
||||
it('handles stop when timer is not running', () => {
|
||||
const timer = new Timer();
|
||||
expect(() => timer.stop()).not.toThrow();
|
||||
expect(timer.elapsed).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,350 @@
|
||||
import {
|
||||
frontendURL,
|
||||
conversationUrl,
|
||||
isValidURL,
|
||||
conversationListPageURL,
|
||||
getArticleSearchURL,
|
||||
hasValidAvatarUrl,
|
||||
timeStampAppendedURL,
|
||||
getHostNameFromURL,
|
||||
extractFilenameFromUrl,
|
||||
sanitizeAllowedDomains,
|
||||
} from '../URLHelper';
|
||||
|
||||
describe('#URL Helpers', () => {
|
||||
describe('conversationListPageURL', () => {
|
||||
it('should return url to dashboard', () => {
|
||||
expect(conversationListPageURL({ accountId: 1 })).toBe(
|
||||
'/app/accounts/1/dashboard'
|
||||
);
|
||||
});
|
||||
it('should return url to inbox', () => {
|
||||
expect(conversationListPageURL({ accountId: 1, inboxId: 1 })).toBe(
|
||||
'/app/accounts/1/inbox/1'
|
||||
);
|
||||
});
|
||||
it('should return url to label', () => {
|
||||
expect(conversationListPageURL({ accountId: 1, label: 'support' })).toBe(
|
||||
'/app/accounts/1/label/support'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return url to team', () => {
|
||||
expect(conversationListPageURL({ accountId: 1, teamId: 1 })).toBe(
|
||||
'/app/accounts/1/team/1'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return url to custom view', () => {
|
||||
expect(conversationListPageURL({ accountId: 1, customViewId: 1 })).toBe(
|
||||
'/app/accounts/1/custom_view/1'
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('conversationUrl', () => {
|
||||
it('should return direct conversation URL if activeInbox is nil', () => {
|
||||
expect(conversationUrl({ accountId: 1, id: 1 })).toBe(
|
||||
'accounts/1/conversations/1'
|
||||
);
|
||||
});
|
||||
it('should return inbox conversation URL if activeInbox is not nil', () => {
|
||||
expect(conversationUrl({ accountId: 1, id: 1, activeInbox: 2 })).toBe(
|
||||
'accounts/1/inbox/2/conversations/1'
|
||||
);
|
||||
});
|
||||
it('should return correct conversation URL if label is active', () => {
|
||||
expect(
|
||||
conversationUrl({ accountId: 1, label: 'customer-support', id: 1 })
|
||||
).toBe('accounts/1/label/customer-support/conversations/1');
|
||||
});
|
||||
it('should return correct conversation URL if team Id is available', () => {
|
||||
expect(conversationUrl({ accountId: 1, teamId: 1, id: 1 })).toBe(
|
||||
'accounts/1/team/1/conversations/1'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('frontendURL', () => {
|
||||
it('should return url without params if params passed is nil', () => {
|
||||
expect(frontendURL('main', null)).toBe('/app/main');
|
||||
});
|
||||
it('should return url without params if params passed is not nil', () => {
|
||||
expect(frontendURL('main', { ping: 'pong' })).toBe('/app/main?ping=pong');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidURL', () => {
|
||||
it('should return true if valid url is passed', () => {
|
||||
expect(isValidURL('https://chatwoot.com')).toBe(true);
|
||||
});
|
||||
it('should return false if invalid url is passed', () => {
|
||||
expect(isValidURL('alert.window')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getArticleSearchURL', () => {
|
||||
it('should generate a basic URL without optional parameters', () => {
|
||||
const url = getArticleSearchURL({
|
||||
portalSlug: 'news',
|
||||
pageNumber: 1,
|
||||
locale: 'en',
|
||||
host: 'myurl.com',
|
||||
});
|
||||
expect(url).toBe('myurl.com/news/articles?page=1&locale=en');
|
||||
});
|
||||
|
||||
it('should include status parameter if provided', () => {
|
||||
const url = getArticleSearchURL({
|
||||
portalSlug: 'news',
|
||||
pageNumber: 1,
|
||||
locale: 'en',
|
||||
status: 'published',
|
||||
host: 'myurl.com',
|
||||
});
|
||||
expect(url).toBe(
|
||||
'myurl.com/news/articles?page=1&locale=en&status=published'
|
||||
);
|
||||
});
|
||||
|
||||
it('should include author_id parameter if provided', () => {
|
||||
const url = getArticleSearchURL({
|
||||
portalSlug: 'news',
|
||||
pageNumber: 1,
|
||||
locale: 'en',
|
||||
authorId: 123,
|
||||
host: 'myurl.com',
|
||||
});
|
||||
expect(url).toBe(
|
||||
'myurl.com/news/articles?page=1&locale=en&author_id=123'
|
||||
);
|
||||
});
|
||||
|
||||
it('should include category_slug parameter if provided', () => {
|
||||
const url = getArticleSearchURL({
|
||||
portalSlug: 'news',
|
||||
pageNumber: 1,
|
||||
locale: 'en',
|
||||
categorySlug: 'technology',
|
||||
host: 'myurl.com',
|
||||
});
|
||||
expect(url).toBe(
|
||||
'myurl.com/news/articles?page=1&locale=en&category_slug=technology'
|
||||
);
|
||||
});
|
||||
|
||||
it('should include sort parameter if provided', () => {
|
||||
const url = getArticleSearchURL({
|
||||
portalSlug: 'news',
|
||||
pageNumber: 1,
|
||||
locale: 'en',
|
||||
sort: 'views',
|
||||
host: 'myurl.com',
|
||||
});
|
||||
expect(url).toBe('myurl.com/news/articles?page=1&locale=en&sort=views');
|
||||
});
|
||||
|
||||
it('should handle multiple optional parameters', () => {
|
||||
const url = getArticleSearchURL({
|
||||
portalSlug: 'news',
|
||||
pageNumber: 1,
|
||||
locale: 'en',
|
||||
status: 'draft',
|
||||
authorId: 456,
|
||||
categorySlug: 'science',
|
||||
sort: 'views',
|
||||
host: 'myurl.com',
|
||||
});
|
||||
expect(url).toBe(
|
||||
'myurl.com/news/articles?page=1&locale=en&status=draft&author_id=456&category_slug=science&sort=views'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle missing optional parameters gracefully', () => {
|
||||
const url = getArticleSearchURL({
|
||||
portalSlug: 'news',
|
||||
pageNumber: 1,
|
||||
locale: 'en',
|
||||
host: 'myurl.com',
|
||||
});
|
||||
expect(url).toBe('myurl.com/news/articles?page=1&locale=en');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasValidAvatarUrl', () => {
|
||||
test('should return true for valid non-Gravatar URL', () => {
|
||||
expect(hasValidAvatarUrl('https://chatwoot.com/avatar.jpg')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false for a Gravatar URL (www.gravatar.com)', () => {
|
||||
expect(hasValidAvatarUrl('https://www.gravatar.com/avatar.jpg')).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test('should return false for a Gravatar URL (gravatar)', () => {
|
||||
expect(hasValidAvatarUrl('https://gravatar/avatar.jpg')).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle invalid URL', () => {
|
||||
expect(hasValidAvatarUrl('invalid-url')).toBe(false); // or expect an error, depending on function design
|
||||
});
|
||||
|
||||
test('should return false for empty or undefined URL', () => {
|
||||
expect(hasValidAvatarUrl('')).toBe(false);
|
||||
expect(hasValidAvatarUrl()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeStampAppendedURL', () => {
|
||||
const FIXED_TIMESTAMP = 1234567890000;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(Date, 'now').mockImplementation(() => FIXED_TIMESTAMP);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should append timestamp to a URL without query parameters', () => {
|
||||
const input = 'https://example.com/audio.mp3';
|
||||
const expected = `https://example.com/audio.mp3?t=${FIXED_TIMESTAMP}`;
|
||||
expect(timeStampAppendedURL(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should append timestamp to a URL with existing query parameters', () => {
|
||||
const input = 'https://example.com/audio.mp3?volume=50';
|
||||
const expected = `https://example.com/audio.mp3?volume=50&t=${FIXED_TIMESTAMP}`;
|
||||
expect(timeStampAppendedURL(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should not append timestamp if it already exists', () => {
|
||||
const input = 'https://example.com/audio.mp3?t=9876543210';
|
||||
expect(timeStampAppendedURL(input)).toBe(input);
|
||||
});
|
||||
|
||||
it('should handle URLs with hash fragments', () => {
|
||||
const input = 'https://example.com/audio.mp3#section1';
|
||||
const expected = `https://example.com/audio.mp3?t=${FIXED_TIMESTAMP}#section1`;
|
||||
expect(timeStampAppendedURL(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle complex URLs', () => {
|
||||
const input =
|
||||
'https://example.com/path/to/audio.mp3?key1=value1&key2=value2#fragment';
|
||||
const expected = `https://example.com/path/to/audio.mp3?key1=value1&key2=value2&t=${FIXED_TIMESTAMP}#fragment`;
|
||||
expect(timeStampAppendedURL(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should throw an error for invalid URLs', () => {
|
||||
const input = 'not a valid url';
|
||||
expect(() => timeStampAppendedURL(input)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHostNameFromURL', () => {
|
||||
it('should return the hostname from a valid URL', () => {
|
||||
expect(getHostNameFromURL('https://example.com/path')).toBe(
|
||||
'example.com'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return null for an invalid URL', () => {
|
||||
expect(getHostNameFromURL('not a valid url')).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null for an empty string', () => {
|
||||
expect(getHostNameFromURL('')).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null for undefined input', () => {
|
||||
expect(getHostNameFromURL(undefined)).toBe(null);
|
||||
});
|
||||
|
||||
it('should correctly handle URLs with non-standard TLDs', () => {
|
||||
expect(getHostNameFromURL('https://chatwoot.help')).toBe('chatwoot.help');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractFilenameFromUrl', () => {
|
||||
it('should extract filename from a valid URL', () => {
|
||||
expect(
|
||||
extractFilenameFromUrl('https://example.com/path/to/file.jpg')
|
||||
).toBe('file.jpg');
|
||||
expect(extractFilenameFromUrl('https://example.com/image.png')).toBe(
|
||||
'image.png'
|
||||
);
|
||||
expect(
|
||||
extractFilenameFromUrl(
|
||||
'https://example.com/folder/document.pdf?query=1'
|
||||
)
|
||||
).toBe('document.pdf');
|
||||
expect(
|
||||
extractFilenameFromUrl('https://example.com/file.txt#section')
|
||||
).toBe('file.txt');
|
||||
});
|
||||
|
||||
it('should handle URLs without filename', () => {
|
||||
expect(extractFilenameFromUrl('https://example.com/')).toBe(
|
||||
'https://example.com/'
|
||||
);
|
||||
expect(extractFilenameFromUrl('https://example.com')).toBe(
|
||||
'https://example.com'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle invalid URLs gracefully', () => {
|
||||
expect(extractFilenameFromUrl('not-a-url/file.txt')).toBe('file.txt');
|
||||
expect(extractFilenameFromUrl('invalid-url')).toBe('invalid-url');
|
||||
});
|
||||
|
||||
it('should handle edge cases', () => {
|
||||
expect(extractFilenameFromUrl('')).toBe('');
|
||||
expect(extractFilenameFromUrl(null)).toBe(null);
|
||||
expect(extractFilenameFromUrl(undefined)).toBe(undefined);
|
||||
expect(extractFilenameFromUrl(123)).toBe(123);
|
||||
});
|
||||
|
||||
it('should handle URLs with query parameters and fragments', () => {
|
||||
expect(
|
||||
extractFilenameFromUrl(
|
||||
'https://example.com/file.jpg?size=large&format=png'
|
||||
)
|
||||
).toBe('file.jpg');
|
||||
expect(
|
||||
extractFilenameFromUrl('https://example.com/file.pdf#page=1')
|
||||
).toBe('file.pdf');
|
||||
expect(
|
||||
extractFilenameFromUrl('https://example.com/file.doc?v=1#section')
|
||||
).toBe('file.doc');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeAllowedDomains', () => {
|
||||
it('returns empty string for falsy input', () => {
|
||||
expect(sanitizeAllowedDomains('')).toBe('');
|
||||
expect(sanitizeAllowedDomains(null)).toBe('');
|
||||
expect(sanitizeAllowedDomains(undefined)).toBe('');
|
||||
});
|
||||
|
||||
it('trims whitespace and converts newlines to commas', () => {
|
||||
const input = ' example.com \n foo.bar\nbar.baz ';
|
||||
expect(sanitizeAllowedDomains(input)).toBe('example.com,foo.bar,bar.baz');
|
||||
});
|
||||
|
||||
it('handles Windows newlines and mixed spacing', () => {
|
||||
const input = ' example.com\r\n\tfoo.bar , bar.baz ';
|
||||
expect(sanitizeAllowedDomains(input)).toBe('example.com,foo.bar,bar.baz');
|
||||
});
|
||||
|
||||
it('removes empty values from repeated commas', () => {
|
||||
const input = ',,example.com,,foo.bar,,';
|
||||
expect(sanitizeAllowedDomains(input)).toBe('example.com,foo.bar');
|
||||
});
|
||||
|
||||
it('lowercases entries and de-duplicates preserving order', () => {
|
||||
const input = 'Example.com,FOO.bar,example.com,Bar.Baz,foo.BAR';
|
||||
expect(sanitizeAllowedDomains(input)).toBe('example.com,foo.bar,bar.baz');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { describe, it, beforeEach, expect, vi } from 'vitest';
|
||||
import ActionCableConnector from '../actionCable';
|
||||
|
||||
vi.mock('shared/helpers/mitt', () => ({
|
||||
emitter: {
|
||||
emit: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('dashboard/composables/useImpersonation', () => ({
|
||||
useImpersonation: () => ({
|
||||
isImpersonating: { value: false },
|
||||
}),
|
||||
}));
|
||||
|
||||
global.chatwootConfig = {
|
||||
websocketURL: 'wss://test.chatwoot.com',
|
||||
};
|
||||
|
||||
describe('ActionCableConnector - Copilot Tests', () => {
|
||||
let store;
|
||||
let actionCable;
|
||||
let mockDispatch;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockDispatch = vi.fn();
|
||||
store = {
|
||||
$store: {
|
||||
dispatch: mockDispatch,
|
||||
getters: {
|
||||
getCurrentAccountId: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
actionCable = ActionCableConnector.init(store.$store, 'test-token');
|
||||
});
|
||||
describe('copilot event handlers', () => {
|
||||
it('should register the copilot.message.created event handler', () => {
|
||||
expect(Object.keys(actionCable.events)).toContain(
|
||||
'copilot.message.created'
|
||||
);
|
||||
expect(actionCable.events['copilot.message.created']).toBe(
|
||||
actionCable.onCopilotMessageCreated
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle the copilot.message.created event through the ActionCable system', () => {
|
||||
const copilotData = {
|
||||
id: 2,
|
||||
content: 'This is a copilot message from ActionCable',
|
||||
conversation_id: 456,
|
||||
created_at: '2025-05-27T15:58:04-06:00',
|
||||
account_id: 1,
|
||||
};
|
||||
actionCable.onReceived({
|
||||
event: 'copilot.message.created',
|
||||
data: copilotData,
|
||||
});
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
'copilotMessages/upsert',
|
||||
copilotData
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import actionQueryGenerator from '../actionQueryGenerator';
|
||||
|
||||
const testData = [
|
||||
{
|
||||
action_name: 'add_label',
|
||||
action_params: [{ id: 'testlabel', name: 'testlabel' }],
|
||||
},
|
||||
{
|
||||
action_name: 'assign_team',
|
||||
action_params: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'sales team',
|
||||
description: 'This is our internal sales team',
|
||||
allow_auto_assign: true,
|
||||
account_id: 1,
|
||||
is_member: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const finalResult = [
|
||||
{
|
||||
action_name: 'add_label',
|
||||
action_params: ['testlabel'],
|
||||
},
|
||||
{
|
||||
action_name: 'assign_team',
|
||||
action_params: [1],
|
||||
},
|
||||
];
|
||||
|
||||
describe('#actionQueryGenerator', () => {
|
||||
it('returns the correct format of filter query', () => {
|
||||
expect(actionQueryGenerator(testData)).toEqual(finalResult);
|
||||
expect(
|
||||
actionQueryGenerator(testData).every(i => Array.isArray(i.action_params))
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
getAgentsByAvailability,
|
||||
getSortedAgentsByAvailability,
|
||||
getAgentsByUpdatedPresence,
|
||||
} from '../agentHelper';
|
||||
import {
|
||||
allAgentsData,
|
||||
onlineAgentsData,
|
||||
busyAgentsData,
|
||||
offlineAgentsData,
|
||||
sortedByAvailability,
|
||||
formattedAgentsByPresenceOnline,
|
||||
formattedAgentsByPresenceOffline,
|
||||
} from 'dashboard/helper/specs/fixtures/agentFixtures';
|
||||
|
||||
describe('agentHelper', () => {
|
||||
describe('getAgentsByAvailability', () => {
|
||||
it('returns agents by availability', () => {
|
||||
expect(getAgentsByAvailability(allAgentsData, 'online')).toEqual(
|
||||
onlineAgentsData
|
||||
);
|
||||
expect(getAgentsByAvailability(allAgentsData, 'busy')).toEqual(
|
||||
busyAgentsData
|
||||
);
|
||||
expect(getAgentsByAvailability(allAgentsData, 'offline')).toEqual(
|
||||
offlineAgentsData
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSortedAgentsByAvailability', () => {
|
||||
it('returns sorted agents by availability', () => {
|
||||
expect(getSortedAgentsByAvailability(allAgentsData)).toEqual(
|
||||
sortedByAvailability
|
||||
);
|
||||
});
|
||||
|
||||
it('returns an empty array when given an empty input', () => {
|
||||
expect(getSortedAgentsByAvailability([])).toEqual([]);
|
||||
});
|
||||
|
||||
it('maintains the order of agents with the same availability status', () => {
|
||||
const result = getSortedAgentsByAvailability(allAgentsData);
|
||||
expect(result[2].name).toBe('Honey Bee');
|
||||
expect(result[3].name).toBe('Samuel Keta');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAgentsByUpdatedPresence', () => {
|
||||
it('returns agents with updated presence', () => {
|
||||
const currentUser = {
|
||||
id: 1,
|
||||
accounts: [{ id: 1, availability_status: 'offline' }],
|
||||
};
|
||||
const currentAccountId = 1;
|
||||
|
||||
expect(
|
||||
getAgentsByUpdatedPresence(
|
||||
formattedAgentsByPresenceOnline,
|
||||
currentUser,
|
||||
currentAccountId
|
||||
)
|
||||
).toEqual(formattedAgentsByPresenceOffline);
|
||||
});
|
||||
|
||||
it('does not modify other agents presence', () => {
|
||||
const currentUser = {
|
||||
id: 2,
|
||||
accounts: [{ id: 1, availability_status: 'offline' }],
|
||||
};
|
||||
const currentAccountId = 1;
|
||||
|
||||
expect(
|
||||
getAgentsByUpdatedPresence(
|
||||
formattedAgentsByPresenceOnline,
|
||||
currentUser,
|
||||
currentAccountId
|
||||
)
|
||||
).toEqual(formattedAgentsByPresenceOnline);
|
||||
});
|
||||
|
||||
it('handles empty agent list', () => {
|
||||
const currentUser = {
|
||||
id: 1,
|
||||
accounts: [{ id: 1, availability_status: 'offline' }],
|
||||
};
|
||||
const currentAccountId = 1;
|
||||
|
||||
expect(
|
||||
getAgentsByUpdatedPresence([], currentUser, currentAccountId)
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,194 @@
|
||||
import {
|
||||
extractChangedAccountUserValues,
|
||||
generateTranslationPayload,
|
||||
generateLogActionKey,
|
||||
} from '../auditlogHelper'; // import the functions
|
||||
|
||||
describe('Helper functions', () => {
|
||||
const agentList = [
|
||||
{ id: 1, name: 'Agent 1' },
|
||||
{ id: 2, name: 'Agent 2' },
|
||||
{ id: 3, name: 'Agent 3' },
|
||||
];
|
||||
|
||||
describe('extractChangedAccountUserValues', () => {
|
||||
it('should correctly extract values when role is changed', () => {
|
||||
const changes = {
|
||||
role: [0, 1],
|
||||
};
|
||||
const { changes: extractedChanges, values } =
|
||||
extractChangedAccountUserValues(changes);
|
||||
expect(extractedChanges).toEqual(['role']);
|
||||
expect(values).toEqual(['administrator']);
|
||||
});
|
||||
|
||||
it('should correctly extract values when availability is changed', () => {
|
||||
const changes = {
|
||||
availability: [0, 2],
|
||||
};
|
||||
const { changes: extractedChanges, values } =
|
||||
extractChangedAccountUserValues(changes);
|
||||
expect(extractedChanges).toEqual(['availability']);
|
||||
expect(values).toEqual(['busy']);
|
||||
});
|
||||
|
||||
it('should correctly extract values when both are changed', () => {
|
||||
const changes = {
|
||||
role: [1, 0],
|
||||
availability: [1, 2],
|
||||
};
|
||||
const { changes: extractedChanges, values } =
|
||||
extractChangedAccountUserValues(changes);
|
||||
expect(extractedChanges).toEqual(['role', 'availability']);
|
||||
expect(values).toEqual(['agent', 'busy']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateTranslationPayload', () => {
|
||||
it('should handle AccountUser create', () => {
|
||||
const auditLogItem = {
|
||||
auditable_type: 'AccountUser',
|
||||
action: 'create',
|
||||
user_id: 1,
|
||||
auditable_id: 123,
|
||||
audited_changes: {
|
||||
user_id: 2,
|
||||
role: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const payload = generateTranslationPayload(auditLogItem, agentList);
|
||||
expect(payload).toEqual({
|
||||
agentName: 'Agent 1',
|
||||
id: 123,
|
||||
invitee: 'Agent 2',
|
||||
role: 'administrator',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle AccountUser update', () => {
|
||||
const auditLogItem = {
|
||||
auditable_type: 'AccountUser',
|
||||
action: 'update',
|
||||
user_id: 1,
|
||||
auditable_id: 123,
|
||||
audited_changes: {
|
||||
user_id: 2,
|
||||
role: [1, 0],
|
||||
availability: [0, 2],
|
||||
},
|
||||
auditable: {
|
||||
user_id: 3,
|
||||
},
|
||||
};
|
||||
|
||||
const payload = generateTranslationPayload(auditLogItem, agentList);
|
||||
expect(payload).toEqual({
|
||||
agentName: 'Agent 1',
|
||||
id: 123,
|
||||
user: 'Agent 3',
|
||||
attributes: ['role', 'availability'],
|
||||
values: ['agent', 'busy'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle InboxMember or TeamMember', () => {
|
||||
const auditLogItemInboxMember = {
|
||||
auditable_type: 'InboxMember',
|
||||
action: 'create',
|
||||
audited_changes: {
|
||||
user_id: 2,
|
||||
},
|
||||
user_id: 1,
|
||||
auditable_id: 789,
|
||||
};
|
||||
|
||||
const payloadInboxMember = generateTranslationPayload(
|
||||
auditLogItemInboxMember,
|
||||
agentList
|
||||
);
|
||||
expect(payloadInboxMember).toEqual({
|
||||
agentName: 'Agent 1',
|
||||
id: 789,
|
||||
user: 'Agent 2',
|
||||
});
|
||||
|
||||
const auditLogItemTeamMember = {
|
||||
auditable_type: 'TeamMember',
|
||||
action: 'create',
|
||||
audited_changes: {
|
||||
user_id: 3,
|
||||
},
|
||||
user_id: 1,
|
||||
auditable_id: 789,
|
||||
};
|
||||
|
||||
const payloadTeamMember = generateTranslationPayload(
|
||||
auditLogItemTeamMember,
|
||||
agentList
|
||||
);
|
||||
expect(payloadTeamMember).toEqual({
|
||||
agentName: 'Agent 1',
|
||||
id: 789,
|
||||
user: 'Agent 3',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle generic case like Team create', () => {
|
||||
const auditLogItem = {
|
||||
auditable_type: 'Team',
|
||||
action: 'create',
|
||||
user_id: 1,
|
||||
auditable_id: 456,
|
||||
};
|
||||
|
||||
const payload = generateTranslationPayload(auditLogItem, agentList);
|
||||
expect(payload).toEqual({
|
||||
agentName: 'Agent 1',
|
||||
id: 456,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateLogActionKey', () => {
|
||||
it('should generate correct action key when user updates self', () => {
|
||||
const auditLogItem = {
|
||||
auditable_type: 'AccountUser',
|
||||
action: 'update',
|
||||
user_id: 1,
|
||||
auditable: {
|
||||
user_id: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const logActionKey = generateLogActionKey(auditLogItem);
|
||||
expect(logActionKey).toEqual('AUDIT_LOGS.ACCOUNT_USER.EDIT.SELF');
|
||||
});
|
||||
|
||||
it('should generate correct action key when user updates other agent', () => {
|
||||
const auditLogItem = {
|
||||
auditable_type: 'AccountUser',
|
||||
action: 'update',
|
||||
user_id: 1,
|
||||
auditable: {
|
||||
user_id: 2,
|
||||
},
|
||||
};
|
||||
|
||||
const logActionKey = generateLogActionKey(auditLogItem);
|
||||
expect(logActionKey).toEqual('AUDIT_LOGS.ACCOUNT_USER.EDIT.OTHER');
|
||||
});
|
||||
|
||||
it('should generate correct action key when updating a deleted user', () => {
|
||||
const auditLogItem = {
|
||||
auditable_type: 'AccountUser',
|
||||
action: 'update',
|
||||
user_id: 1,
|
||||
auditable: null,
|
||||
};
|
||||
|
||||
const logActionKey = generateLogActionKey(auditLogItem);
|
||||
expect(logActionKey).toEqual('AUDIT_LOGS.ACCOUNT_USER.EDIT.DELETED');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,455 @@
|
||||
import * as helpers from 'dashboard/helper/automationHelper';
|
||||
import {
|
||||
OPERATOR_TYPES_1,
|
||||
OPERATOR_TYPES_3,
|
||||
OPERATOR_TYPES_4,
|
||||
} from 'dashboard/routes/dashboard/settings/automation/operators';
|
||||
import {
|
||||
customAttributes,
|
||||
labels,
|
||||
automation,
|
||||
contactAttrs,
|
||||
conversationAttrs,
|
||||
expectedOutputForCustomAttributeGenerator,
|
||||
} from './fixtures/automationFixtures';
|
||||
import { AUTOMATIONS } from 'dashboard/routes/dashboard/settings/automation/constants';
|
||||
|
||||
describe('getCustomAttributeInputType', () => {
|
||||
it('returns the attribute input type', () => {
|
||||
expect(helpers.getCustomAttributeInputType('date')).toEqual('date');
|
||||
expect(helpers.getCustomAttributeInputType('date')).not.toEqual(
|
||||
'some_random_value'
|
||||
);
|
||||
expect(helpers.getCustomAttributeInputType('text')).toEqual('plain_text');
|
||||
expect(helpers.getCustomAttributeInputType('list')).toEqual(
|
||||
'search_select'
|
||||
);
|
||||
expect(helpers.getCustomAttributeInputType('checkbox')).toEqual(
|
||||
'search_select'
|
||||
);
|
||||
expect(helpers.getCustomAttributeInputType('some_random_text')).toEqual(
|
||||
'plain_text'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isACustomAttribute', () => {
|
||||
it('returns the custom attribute value if true', () => {
|
||||
expect(
|
||||
helpers.isACustomAttribute(customAttributes, 'signed_up_at')
|
||||
).toBeTruthy();
|
||||
expect(helpers.isACustomAttribute(customAttributes, 'status')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCustomAttributeListDropdownValues', () => {
|
||||
it('returns the attribute dropdown values', () => {
|
||||
const myListValues = [
|
||||
{ id: 'item1', name: 'item1' },
|
||||
{ id: 'item2', name: 'item2' },
|
||||
{ id: 'item3', name: 'item3' },
|
||||
];
|
||||
expect(
|
||||
helpers.getCustomAttributeListDropdownValues(customAttributes, 'my_list')
|
||||
).toEqual(myListValues);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCustomAttributeCheckbox', () => {
|
||||
it('checks if attribute is a checkbox', () => {
|
||||
expect(
|
||||
helpers.isCustomAttributeCheckbox(customAttributes, 'prime_user')
|
||||
.attribute_display_type
|
||||
).toEqual('checkbox');
|
||||
expect(
|
||||
helpers.isCustomAttributeCheckbox(customAttributes, 'my_check')
|
||||
.attribute_display_type
|
||||
).toEqual('checkbox');
|
||||
expect(
|
||||
helpers.isCustomAttributeCheckbox(customAttributes, 'my_list')
|
||||
).not.toEqual('checkbox');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCustomAttributeList', () => {
|
||||
it('checks if attribute is a list', () => {
|
||||
expect(
|
||||
helpers.isCustomAttributeList(customAttributes, 'my_list')
|
||||
.attribute_display_type
|
||||
).toEqual('list');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOperatorTypes', () => {
|
||||
it('returns the correct custom attribute operators', () => {
|
||||
expect(helpers.getOperatorTypes('list')).toEqual(OPERATOR_TYPES_1);
|
||||
expect(helpers.getOperatorTypes('text')).toEqual(OPERATOR_TYPES_3);
|
||||
expect(helpers.getOperatorTypes('number')).toEqual(OPERATOR_TYPES_1);
|
||||
expect(helpers.getOperatorTypes('link')).toEqual(OPERATOR_TYPES_1);
|
||||
expect(helpers.getOperatorTypes('date')).toEqual(OPERATOR_TYPES_4);
|
||||
expect(helpers.getOperatorTypes('checkbox')).toEqual(OPERATOR_TYPES_1);
|
||||
expect(helpers.getOperatorTypes('some_random')).toEqual(OPERATOR_TYPES_1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateConditionOptions', () => {
|
||||
it('returns expected conditions options array', () => {
|
||||
const testConditions = [
|
||||
{ id: 123, title: 'Fayaz', email: 'test@test.com' },
|
||||
{ title: 'John', id: 324, email: 'test@john.com' },
|
||||
];
|
||||
const expectedConditions = [
|
||||
{ id: 123, name: 'Fayaz' },
|
||||
{ id: 324, name: 'John' },
|
||||
];
|
||||
expect(helpers.generateConditionOptions(testConditions)).toEqual(
|
||||
expectedConditions
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActionOptions', () => {
|
||||
it('returns expected actions options array', () => {
|
||||
const expectedOptions = [
|
||||
{ id: 'testlabel', name: 'testlabel' },
|
||||
{ id: 'snoozes', name: 'snoozes' },
|
||||
];
|
||||
expect(helpers.getActionOptions({ labels, type: 'add_label' })).toEqual(
|
||||
expectedOptions
|
||||
);
|
||||
});
|
||||
|
||||
it('adds None option when addNoneToListFn is provided', () => {
|
||||
const mockAddNoneToListFn = list => [
|
||||
{ id: 'nil', name: 'None' },
|
||||
...(list || []),
|
||||
];
|
||||
|
||||
const agents = [
|
||||
{ id: 1, name: 'Agent 1' },
|
||||
{ id: 2, name: 'Agent 2' },
|
||||
];
|
||||
|
||||
const expectedOptions = [
|
||||
{ id: 'nil', name: 'None' },
|
||||
{ id: 1, name: 'Agent 1' },
|
||||
{ id: 2, name: 'Agent 2' },
|
||||
];
|
||||
|
||||
expect(
|
||||
helpers.getActionOptions({
|
||||
agents,
|
||||
type: 'assign_agent',
|
||||
addNoneToListFn: mockAddNoneToListFn,
|
||||
})
|
||||
).toEqual(expectedOptions);
|
||||
});
|
||||
|
||||
it('does not add None option when addNoneToListFn is not provided', () => {
|
||||
const agents = [
|
||||
{ id: 1, name: 'Agent 1' },
|
||||
{ id: 2, name: 'Agent 2' },
|
||||
];
|
||||
|
||||
expect(
|
||||
helpers.getActionOptions({
|
||||
agents,
|
||||
type: 'assign_agent',
|
||||
})
|
||||
).toEqual(agents);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConditionOptions', () => {
|
||||
it('returns expected conditions options', () => {
|
||||
const testOptions = [
|
||||
{ id: 'open', name: 'Open' },
|
||||
{ id: 'resolved', name: 'Resolved' },
|
||||
{ id: 'pending', name: 'Pending' },
|
||||
{ id: 'snoozed', name: 'Snoozed' },
|
||||
{ id: 'all', name: 'All' },
|
||||
];
|
||||
expect(
|
||||
helpers.getConditionOptions({
|
||||
customAttributes,
|
||||
campaigns: [],
|
||||
statusFilterOptions: testOptions,
|
||||
type: 'status',
|
||||
})
|
||||
).toEqual(testOptions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFileName', () => {
|
||||
it('returns the correct file name', () => {
|
||||
expect(
|
||||
helpers.getFileName(automation.actions[0], automation.files)
|
||||
).toEqual('pfp.jpeg');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefaultConditions', () => {
|
||||
it('returns the resp default condition model', () => {
|
||||
const messageCreatedModel = [
|
||||
{
|
||||
attribute_key: 'message_type',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
},
|
||||
];
|
||||
const genericConditionModel = [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
},
|
||||
];
|
||||
expect(helpers.getDefaultConditions('message_created')).toEqual(
|
||||
messageCreatedModel
|
||||
);
|
||||
expect(helpers.getDefaultConditions()).toEqual(genericConditionModel);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefaultActions', () => {
|
||||
it('returns the resp default action model', () => {
|
||||
const genericActionModel = [
|
||||
{
|
||||
action_name: 'assign_agent',
|
||||
action_params: [],
|
||||
},
|
||||
];
|
||||
expect(helpers.getDefaultActions()).toEqual(genericActionModel);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterCustomAttributes', () => {
|
||||
it('filters the raw custom attributes', () => {
|
||||
const filteredAttributes = [
|
||||
{ key: 'signed_up_at', name: 'Signed Up At', type: 'date' },
|
||||
{ key: 'prime_user', name: 'Prime User', type: 'checkbox' },
|
||||
{ key: 'test', name: 'Test', type: 'text' },
|
||||
{ key: 'link', name: 'Link', type: 'link' },
|
||||
{ key: 'my_list', name: 'My List', type: 'list' },
|
||||
{ key: 'my_check', name: 'My Check', type: 'checkbox' },
|
||||
{ key: 'conlist', name: 'ConList', type: 'list' },
|
||||
{ key: 'asdf', name: 'asdf', type: 'link' },
|
||||
];
|
||||
expect(helpers.filterCustomAttributes(customAttributes)).toEqual(
|
||||
filteredAttributes
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStandardAttributeInputType', () => {
|
||||
it('returns the resp default action model', () => {
|
||||
expect(
|
||||
helpers.getStandardAttributeInputType(
|
||||
AUTOMATIONS,
|
||||
'message_created',
|
||||
'message_type'
|
||||
)
|
||||
).toEqual('search_select');
|
||||
expect(
|
||||
helpers.getStandardAttributeInputType(
|
||||
AUTOMATIONS,
|
||||
'conversation_created',
|
||||
'status'
|
||||
)
|
||||
).toEqual('multi_select');
|
||||
expect(
|
||||
helpers.getStandardAttributeInputType(
|
||||
AUTOMATIONS,
|
||||
'conversation_updated',
|
||||
'referer'
|
||||
)
|
||||
).toEqual('plain_text');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateAutomationPayload', () => {
|
||||
it('returns the resp default action model', () => {
|
||||
const testPayload = {
|
||||
name: 'Test',
|
||||
description: 'This is a test',
|
||||
event_name: 'conversation_created',
|
||||
conditions: [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: [{ id: 'open', name: 'Open' }],
|
||||
query_operator: 'and',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
action_name: 'add_label',
|
||||
action_params: [{ id: 2, name: 'testlabel' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
const expectedPayload = {
|
||||
name: 'Test',
|
||||
description: 'This is a test',
|
||||
event_name: 'conversation_created',
|
||||
conditions: [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: ['open'],
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
action_name: 'add_label',
|
||||
action_params: [2],
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(helpers.generateAutomationPayload(testPayload)).toEqual(
|
||||
expectedPayload
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCustomAttribute', () => {
|
||||
it('returns the resp default action model', () => {
|
||||
const attrs = helpers.filterCustomAttributes(customAttributes);
|
||||
expect(helpers.isCustomAttribute(attrs, 'my_list')).toBeTruthy();
|
||||
expect(helpers.isCustomAttribute(attrs, 'my_check')).toBeTruthy();
|
||||
expect(helpers.isCustomAttribute(attrs, 'signed_up_at')).toBeTruthy();
|
||||
expect(helpers.isCustomAttribute(attrs, 'link')).toBeTruthy();
|
||||
expect(helpers.isCustomAttribute(attrs, 'prime_user')).toBeTruthy();
|
||||
expect(helpers.isCustomAttribute(attrs, 'hello')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateCustomAttributes', () => {
|
||||
it('generates and returns correct condition attribute', () => {
|
||||
expect(
|
||||
helpers.generateCustomAttributes(
|
||||
conversationAttrs,
|
||||
contactAttrs,
|
||||
'Conversation Custom Attributes',
|
||||
'Contact Custom Attributes'
|
||||
)
|
||||
).toEqual(expectedOutputForCustomAttributeGenerator);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAttributes', () => {
|
||||
it('returns the conditions for the given automation type', () => {
|
||||
const result = helpers.getAttributes(AUTOMATIONS, 'message_created');
|
||||
expect(result).toEqual(AUTOMATIONS.message_created.conditions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAttributes', () => {
|
||||
it('returns the conditions for the given automation type', () => {
|
||||
const result = helpers.getAttributes(AUTOMATIONS, 'message_created');
|
||||
expect(result).toEqual(AUTOMATIONS.message_created.conditions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAutomationType', () => {
|
||||
it('returns the automation type for the given key', () => {
|
||||
const mockAutomation = { event_name: 'message_created' };
|
||||
const result = helpers.getAutomationType(
|
||||
AUTOMATIONS,
|
||||
mockAutomation,
|
||||
'message_type'
|
||||
);
|
||||
expect(result).toEqual(
|
||||
AUTOMATIONS.message_created.conditions.find(c => c.key === 'message_type')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInputType', () => {
|
||||
it('returns the input type for a custom attribute', () => {
|
||||
const mockAutomation = { event_name: 'message_created' };
|
||||
const result = helpers.getInputType(
|
||||
customAttributes,
|
||||
AUTOMATIONS,
|
||||
mockAutomation,
|
||||
'signed_up_at'
|
||||
);
|
||||
expect(result).toEqual('date');
|
||||
});
|
||||
|
||||
it('returns the input type for a standard attribute', () => {
|
||||
const mockAutomation = { event_name: 'message_created' };
|
||||
const result = helpers.getInputType(
|
||||
customAttributes,
|
||||
AUTOMATIONS,
|
||||
mockAutomation,
|
||||
'message_type'
|
||||
);
|
||||
expect(result).toEqual('search_select');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOperators', () => {
|
||||
it('returns operators for a custom attribute in edit mode', () => {
|
||||
const mockAutomation = { event_name: 'message_created' };
|
||||
const result = helpers.getOperators(
|
||||
customAttributes,
|
||||
AUTOMATIONS,
|
||||
mockAutomation,
|
||||
'edit',
|
||||
'signed_up_at'
|
||||
);
|
||||
expect(result).toEqual(OPERATOR_TYPES_4);
|
||||
});
|
||||
|
||||
it('returns operators for a standard attribute', () => {
|
||||
const mockAutomation = { event_name: 'message_created' };
|
||||
const result = helpers.getOperators(
|
||||
customAttributes,
|
||||
AUTOMATIONS,
|
||||
mockAutomation,
|
||||
'create',
|
||||
'message_type'
|
||||
);
|
||||
expect(result).toEqual(
|
||||
AUTOMATIONS.message_created.conditions.find(c => c.key === 'message_type')
|
||||
.filterOperators
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCustomAttributeType', () => {
|
||||
it('returns the custom attribute type for the given key', () => {
|
||||
const mockAutomation = { event_name: 'message_created' };
|
||||
const result = helpers.getCustomAttributeType(
|
||||
AUTOMATIONS,
|
||||
mockAutomation,
|
||||
'message_type'
|
||||
);
|
||||
expect(result).toEqual(
|
||||
AUTOMATIONS.message_created.conditions.find(c => c.key === 'message_type')
|
||||
.customAttributeType
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('showActionInput', () => {
|
||||
it('returns false for send_email_to_team and send_message actions', () => {
|
||||
expect(helpers.showActionInput([], 'send_email_to_team')).toBe(false);
|
||||
expect(helpers.showActionInput([], 'send_message')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true if the action has an input type', () => {
|
||||
const mockActionTypes = [{ key: 'add_label', inputType: 'select' }];
|
||||
expect(helpers.showActionInput(mockActionTypes, 'add_label')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if the action does not have an input type', () => {
|
||||
const mockActionTypes = [{ key: 'some_action', inputType: null }];
|
||||
expect(helpers.showActionInput(mockActionTypes, 'some_action')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,193 @@
|
||||
import {
|
||||
getTypingUsersText,
|
||||
createPendingMessage,
|
||||
convertToAttributeSlug,
|
||||
convertToCategorySlug,
|
||||
convertToPortalSlug,
|
||||
sanitizeVariableSearchKey,
|
||||
formatToTitleCase,
|
||||
} from '../commons';
|
||||
|
||||
describe('#getTypingUsersText', () => {
|
||||
it('returns the correct text is there is only one typing user', () => {
|
||||
expect(getTypingUsersText([{ name: 'Pranav' }])).toEqual([
|
||||
'TYPING.ONE',
|
||||
{ user: 'Pranav' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns the correct text is there are two typing users', () => {
|
||||
expect(
|
||||
getTypingUsersText([{ name: 'Pranav' }, { name: 'Nithin' }])
|
||||
).toEqual(['TYPING.TWO', { user: 'Pranav', secondUser: 'Nithin' }]);
|
||||
});
|
||||
|
||||
it('returns the correct text is there are more than two users are typing', () => {
|
||||
expect(
|
||||
getTypingUsersText([
|
||||
{ name: 'Pranav' },
|
||||
{ name: 'Nithin' },
|
||||
{ name: 'Subin' },
|
||||
{ name: 'Sojan' },
|
||||
])
|
||||
).toEqual(['TYPING.MULTIPLE', { user: 'Pranav', count: 3 }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#createPendingMessage', () => {
|
||||
const message = {
|
||||
message: 'hi',
|
||||
};
|
||||
it('returns the pending message with expected new keys', () => {
|
||||
expect(createPendingMessage(message)).toMatchObject({
|
||||
content: expect.anything(),
|
||||
id: expect.anything(),
|
||||
status: expect.anything(),
|
||||
echo_id: expect.anything(),
|
||||
created_at: expect.anything(),
|
||||
message_type: expect.anything(),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the pending message with status progress', () => {
|
||||
expect(createPendingMessage(message)).toMatchObject({
|
||||
status: 'progress',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the pending message with same id and echo_id', () => {
|
||||
const pending = createPendingMessage(message);
|
||||
expect(pending).toMatchObject({
|
||||
echo_id: pending.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the pending message with attachment key if file is passed', () => {
|
||||
const messageWithFile = {
|
||||
message: 'hi',
|
||||
file: {},
|
||||
};
|
||||
expect(createPendingMessage(messageWithFile)).toMatchObject({
|
||||
content: expect.anything(),
|
||||
id: expect.anything(),
|
||||
status: expect.anything(),
|
||||
echo_id: expect.anything(),
|
||||
created_at: expect.anything(),
|
||||
message_type: expect.anything(),
|
||||
attachments: [{ id: expect.anything() }],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the pending message to have one attachment', () => {
|
||||
const messageWithFile = {
|
||||
message: 'hi',
|
||||
file: {},
|
||||
};
|
||||
const pending = createPendingMessage(messageWithFile);
|
||||
expect(pending.attachments.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertToAttributeSlug', () => {
|
||||
it('should convert to slug', () => {
|
||||
expect(convertToAttributeSlug('Test@%^&*(){}>.!@`~_ ing')).toBe(
|
||||
'test__ing'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertToCategorySlug', () => {
|
||||
it('should convert to slug', () => {
|
||||
expect(convertToCategorySlug('User profile guide')).toBe(
|
||||
'user-profile-guide'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertToPortalSlug', () => {
|
||||
it('should convert to slug', () => {
|
||||
expect(convertToPortalSlug('Room rental')).toBe('room-rental');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeVariableSearchKey', () => {
|
||||
it('removes braces', () => {
|
||||
expect(sanitizeVariableSearchKey('{{contact.name}}')).toBe('contact.name');
|
||||
});
|
||||
|
||||
it('removes right braces', () => {
|
||||
expect(sanitizeVariableSearchKey('contact.name}}')).toBe('contact.name');
|
||||
});
|
||||
|
||||
it('removes braces, comma and whitespace', () => {
|
||||
expect(sanitizeVariableSearchKey(' {{contact.name }},')).toBe(
|
||||
'contact.name'
|
||||
);
|
||||
});
|
||||
|
||||
it('trims whitespace', () => {
|
||||
expect(sanitizeVariableSearchKey(' contact.name ')).toBe('contact.name');
|
||||
});
|
||||
|
||||
it('handles multiple commas', () => {
|
||||
expect(sanitizeVariableSearchKey('{{contact.name}},,')).toBe(
|
||||
'contact.name'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns empty string when only braces/commas/whitespace', () => {
|
||||
expect(sanitizeVariableSearchKey(' { }, , ')).toBe('');
|
||||
});
|
||||
|
||||
it('returns empty string for undefined input', () => {
|
||||
expect(sanitizeVariableSearchKey()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatToTitleCase', () => {
|
||||
it('converts underscore-separated string to title case', () => {
|
||||
expect(formatToTitleCase('round_robin')).toBe('Round Robin');
|
||||
});
|
||||
|
||||
it('converts single word to title case', () => {
|
||||
expect(formatToTitleCase('priority')).toBe('Priority');
|
||||
});
|
||||
|
||||
it('converts multiple underscores to title case', () => {
|
||||
expect(formatToTitleCase('auto_assignment_policy')).toBe(
|
||||
'Auto Assignment Policy'
|
||||
);
|
||||
});
|
||||
|
||||
it('handles already capitalized words', () => {
|
||||
expect(formatToTitleCase('HIGH_PRIORITY')).toBe('HIGH PRIORITY');
|
||||
});
|
||||
|
||||
it('handles mixed case with underscores', () => {
|
||||
expect(formatToTitleCase('first_Name_last')).toBe('First Name Last');
|
||||
});
|
||||
|
||||
it('handles empty string', () => {
|
||||
expect(formatToTitleCase('')).toBe('');
|
||||
});
|
||||
|
||||
it('handles null input', () => {
|
||||
expect(formatToTitleCase(null)).toBe('');
|
||||
});
|
||||
|
||||
it('handles undefined input', () => {
|
||||
expect(formatToTitleCase(undefined)).toBe('');
|
||||
});
|
||||
|
||||
it('handles string without underscores', () => {
|
||||
expect(formatToTitleCase('hello')).toBe('Hello');
|
||||
});
|
||||
|
||||
it('handles string with numbers', () => {
|
||||
expect(formatToTitleCase('priority_1_high')).toBe('Priority 1 High');
|
||||
});
|
||||
|
||||
it('handles leading and trailing underscores', () => {
|
||||
expect(formatToTitleCase('_leading_trailing_')).toBe('Leading Trailing');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
filterDuplicateSourceMessages,
|
||||
getLastMessage,
|
||||
getReadMessages,
|
||||
getUnreadMessages,
|
||||
} from '../conversationHelper';
|
||||
import {
|
||||
conversationData,
|
||||
lastMessageData,
|
||||
readMessagesData,
|
||||
unReadMessagesData,
|
||||
} from './fixtures/conversationFixtures';
|
||||
|
||||
describe('conversationHelper', () => {
|
||||
describe('#filterDuplicateSourceMessages', () => {
|
||||
it('returns messages without duplicate source_id and all messages without source_id', () => {
|
||||
const input = [
|
||||
{ source_id: null, id: 1 },
|
||||
{ source_id: '', id: 2 },
|
||||
{ id: 3 },
|
||||
{ source_id: 'wa_1', id: 4 },
|
||||
{ source_id: 'wa_1', id: 5 },
|
||||
{ source_id: 'wa_1', id: 6 },
|
||||
{ source_id: 'wa_2', id: 7 },
|
||||
{ source_id: 'wa_2', id: 8 },
|
||||
{ source_id: 'wa_3', id: 9 },
|
||||
];
|
||||
const expected = [
|
||||
{ source_id: null, id: 1 },
|
||||
{ source_id: '', id: 2 },
|
||||
{ id: 3 },
|
||||
{ source_id: 'wa_1', id: 4 },
|
||||
{ source_id: 'wa_2', id: 7 },
|
||||
{ source_id: 'wa_3', id: 9 },
|
||||
];
|
||||
expect(filterDuplicateSourceMessages(input)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#readMessages', () => {
|
||||
it('should return read messages if conversation is passed', () => {
|
||||
expect(
|
||||
getReadMessages(
|
||||
conversationData.messages,
|
||||
conversationData.agent_last_seen_at
|
||||
)
|
||||
).toEqual(readMessagesData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#unReadMessages', () => {
|
||||
it('should return unread messages if conversation is passed', () => {
|
||||
expect(
|
||||
getUnreadMessages(
|
||||
conversationData.messages,
|
||||
conversationData.agent_last_seen_at
|
||||
)
|
||||
).toEqual(unReadMessagesData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#lastMessage', () => {
|
||||
it("should return last activity message if both api and store doesn't have other messages", () => {
|
||||
const testConversation = {
|
||||
messages: [conversationData.messages[0]],
|
||||
last_non_activity_message: null,
|
||||
};
|
||||
expect(getLastMessage(testConversation)).toEqual(
|
||||
testConversation.messages[0]
|
||||
);
|
||||
});
|
||||
|
||||
it('should return message from store if store has latest message', () => {
|
||||
const testConversation = {
|
||||
messages: [],
|
||||
last_non_activity_message: lastMessageData,
|
||||
};
|
||||
expect(getLastMessage(testConversation)).toEqual(lastMessageData);
|
||||
});
|
||||
|
||||
it('should return last non activity message from store if api value is empty', () => {
|
||||
const testConversation = {
|
||||
messages: [conversationData.messages[0], conversationData.messages[1]],
|
||||
last_non_activity_message: null,
|
||||
};
|
||||
expect(getLastMessage(testConversation)).toEqual(
|
||||
testConversation.messages[1]
|
||||
);
|
||||
});
|
||||
|
||||
it("should return last non activity message from store if store doesn't have any messages", () => {
|
||||
const testConversation = {
|
||||
messages: [conversationData.messages[1], conversationData.messages[2]],
|
||||
last_non_activity_message: conversationData.messages[0],
|
||||
};
|
||||
expect(getLastMessage(testConversation)).toEqual(
|
||||
testConversation.messages[1]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,305 @@
|
||||
import {
|
||||
getAttributeInputType,
|
||||
getInputType,
|
||||
getValuesName,
|
||||
getValuesForStatus,
|
||||
getValuesForFilter,
|
||||
generateValuesForEditCustomViews,
|
||||
generateCustomAttributesInputType,
|
||||
} from '../customViewsHelper';
|
||||
import advancedFilterTypes from 'dashboard/components/widgets/conversation/advancedFilterItems/index';
|
||||
|
||||
describe('customViewsHelper', () => {
|
||||
describe('#getInputType', () => {
|
||||
it('should return plain_text if key is created_at or last_activity_at and operator is days_before', () => {
|
||||
const filterTypes = [
|
||||
{ attributeKey: 'created_at', inputType: 'date' },
|
||||
{ attributeKey: 'last_activity_at', inputType: 'date' },
|
||||
];
|
||||
expect(getInputType('created_at', 'days_before', filterTypes)).toEqual(
|
||||
'plain_text'
|
||||
);
|
||||
expect(
|
||||
getInputType('last_activity_at', 'days_before', filterTypes)
|
||||
).toEqual('plain_text');
|
||||
});
|
||||
|
||||
it('should return inputType if key is not created_at or last_activity_at', () => {
|
||||
const filterTypes = [
|
||||
{ attributeKey: 'created_at', inputType: 'date' },
|
||||
{ attributeKey: 'last_activity_at', inputType: 'date' },
|
||||
{ attributeKey: 'test', inputType: 'string' },
|
||||
];
|
||||
expect(getInputType('test', 'days_before', filterTypes)).toEqual(
|
||||
'string'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return undefined if key is not created_at or last_activity_at and inputType is not present', () => {
|
||||
const filterTypes = [
|
||||
{ attributeKey: 'created_at', inputType: 'date' },
|
||||
{ attributeKey: 'last_activity_at', inputType: 'date' },
|
||||
{ attributeKey: 'test', inputType: 'string' },
|
||||
];
|
||||
expect(getInputType('test', 'days_before', filterTypes)).toEqual(
|
||||
'string'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getAttributeInputType', () => {
|
||||
it('should return multi_select if attribute_display_type is checkbox or list', () => {
|
||||
const allCustomAttributes = [
|
||||
{ attribute_key: 'test', attribute_display_type: 'checkbox' },
|
||||
{ attribute_key: 'test2', attribute_display_type: 'list' },
|
||||
];
|
||||
expect(getAttributeInputType('test', allCustomAttributes)).toEqual(
|
||||
'multi_select'
|
||||
);
|
||||
expect(getAttributeInputType('test2', allCustomAttributes)).toEqual(
|
||||
'multi_select'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return string if attribute_display_type is text, number, date or link', () => {
|
||||
const allCustomAttributes = [
|
||||
{ attribute_key: 'test', attribute_display_type: 'text' },
|
||||
{ attribute_key: 'test2', attribute_display_type: 'number' },
|
||||
{ attribute_key: 'test3', attribute_display_type: 'date' },
|
||||
{ attribute_key: 'test4', attribute_display_type: 'link' },
|
||||
];
|
||||
expect(getAttributeInputType('test', allCustomAttributes)).toEqual(
|
||||
'string'
|
||||
);
|
||||
expect(getAttributeInputType('test2', allCustomAttributes)).toEqual(
|
||||
'string'
|
||||
);
|
||||
expect(getAttributeInputType('test3', allCustomAttributes)).toEqual(
|
||||
'string'
|
||||
);
|
||||
expect(getAttributeInputType('test4', allCustomAttributes)).toEqual(
|
||||
'string'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getValuesName', () => {
|
||||
it('should return id and name if item is present', () => {
|
||||
const list = [{ id: 1, name: 'test' }];
|
||||
const idKey = 'id';
|
||||
const nameKey = 'name';
|
||||
const values = [1];
|
||||
expect(getValuesName(values, list, idKey, nameKey)).toEqual({
|
||||
id: 1,
|
||||
name: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return id and value if item is not present', () => {
|
||||
const list = [{ id: 1, name: 'test' }];
|
||||
const idKey = 'id';
|
||||
const nameKey = 'name';
|
||||
const values = [2];
|
||||
expect(getValuesName(values, list, idKey, nameKey)).toEqual({
|
||||
id: 2,
|
||||
name: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getValuesForStatus', () => {
|
||||
it('should return id and name if value is present', () => {
|
||||
const values = ['open'];
|
||||
expect(getValuesForStatus(values)).toEqual([
|
||||
{ id: 'open', name: 'open' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return id and name if multiple values are present', () => {
|
||||
const values = ['open', 'resolved'];
|
||||
expect(getValuesForStatus(values)).toEqual([
|
||||
{ id: 'open', name: 'open' },
|
||||
{ id: 'resolved', name: 'resolved' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getValuesForFilter', () => {
|
||||
it('should return id and name if attribute_key is status', () => {
|
||||
const filter = { attribute_key: 'status', values: ['open', 'resolved'] };
|
||||
const params = {};
|
||||
expect(getValuesForFilter(filter, params)).toEqual([
|
||||
{ id: 'open', name: 'open' },
|
||||
{ id: 'resolved', name: 'resolved' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return id and name if attribute_key is assignee_id', () => {
|
||||
const filter = { attribute_key: 'assignee_id', values: [1] };
|
||||
const params = { agents: [{ id: 1, name: 'test' }] };
|
||||
expect(getValuesForFilter(filter, params)).toEqual({
|
||||
id: 1,
|
||||
name: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return id and name if attribute_key is inbox_id', () => {
|
||||
const filter = { attribute_key: 'inbox_id', values: [1] };
|
||||
const params = { inboxes: [{ id: 1, name: 'test' }] };
|
||||
expect(getValuesForFilter(filter, params)).toEqual({
|
||||
id: 1,
|
||||
name: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return id and name if attribute_key is team_id', () => {
|
||||
const filter = { attribute_key: 'team_id', values: [1] };
|
||||
const params = { teams: [{ id: 1, name: 'test' }] };
|
||||
expect(getValuesForFilter(filter, params)).toEqual({
|
||||
id: 1,
|
||||
name: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return id and title if attribute_key is campaign_id', () => {
|
||||
const filter = { attribute_key: 'campaign_id', values: [1] };
|
||||
const params = { campaigns: [{ id: 1, title: 'test' }] };
|
||||
expect(getValuesForFilter(filter, params)).toEqual({
|
||||
id: 1,
|
||||
name: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return id and title if attribute_key is labels', () => {
|
||||
const filter = { attribute_key: 'labels', values: ['test'] };
|
||||
const params = { labels: [{ title: 'test' }] };
|
||||
expect(getValuesForFilter(filter, params)).toEqual([
|
||||
{ id: 'test', name: 'test' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return id and name if attribute_key is browser_language', () => {
|
||||
const filter = { attribute_key: 'browser_language', values: ['en'] };
|
||||
const params = { languages: [{ id: 'en', name: 'English' }] };
|
||||
expect(getValuesForFilter(filter, params)).toEqual([
|
||||
{ id: 'en', name: 'English' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return id and name if attribute_key is country_code', () => {
|
||||
const filter = { attribute_key: 'country_code', values: ['IN'] };
|
||||
const params = { countries: [{ id: 'IN', name: 'India' }] };
|
||||
expect(getValuesForFilter(filter, params)).toEqual([
|
||||
{ id: 'IN', name: 'India' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return id and name if attribute_key is not present', () => {
|
||||
const filter = { attribute_key: 'test', values: [1] };
|
||||
const params = {};
|
||||
expect(getValuesForFilter(filter, params)).toEqual({
|
||||
id: 1,
|
||||
name: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#generateValuesForEditCustomViews', () => {
|
||||
it('should return id and name if inboxType is multi_select or search_select', () => {
|
||||
const filter = {
|
||||
attribute_key: 'assignee_id',
|
||||
filter_operator: 'and',
|
||||
values: [1],
|
||||
};
|
||||
const params = {
|
||||
filterTypes: advancedFilterTypes,
|
||||
allCustomAttributes: [],
|
||||
agents: [{ id: 1, name: 'test' }],
|
||||
};
|
||||
expect(generateValuesForEditCustomViews(filter, params)).toEqual({
|
||||
id: 1,
|
||||
name: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return id and name if inboxType is not multi_select or search_select', () => {
|
||||
const filter = {
|
||||
attribute_key: 'assignee_id',
|
||||
filter_operator: 'and',
|
||||
values: [1],
|
||||
};
|
||||
const params = {
|
||||
filterTypes: advancedFilterTypes,
|
||||
allCustomAttributes: [],
|
||||
agents: [{ id: 1, name: 'test' }],
|
||||
};
|
||||
expect(generateValuesForEditCustomViews(filter, params)).toEqual({
|
||||
id: 1,
|
||||
name: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return id and name if inboxType is undefined', () => {
|
||||
const filter = {
|
||||
attribute_key: 'test2',
|
||||
filter_operator: 'and',
|
||||
values: [1],
|
||||
};
|
||||
const params = {
|
||||
filterTypes: advancedFilterTypes,
|
||||
allCustomAttributes: [
|
||||
{ attribute_key: 'test', attribute_display_type: 'checkbox' },
|
||||
{ attribute_key: 'test2', attribute_display_type: 'list' },
|
||||
],
|
||||
};
|
||||
expect(generateValuesForEditCustomViews(filter, params)).toEqual({
|
||||
id: 1,
|
||||
name: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return value as string if filterInputTypes is string', () => {
|
||||
const filter = {
|
||||
attribute_key: 'test',
|
||||
filter_operator: 'and',
|
||||
values: [1],
|
||||
};
|
||||
const params = {
|
||||
filterTypes: advancedFilterTypes,
|
||||
allCustomAttributes: [
|
||||
{ attribute_key: 'test', attribute_display_type: 'date' },
|
||||
{ attribute_key: 'test2', attribute_display_type: 'list' },
|
||||
],
|
||||
};
|
||||
expect(generateValuesForEditCustomViews(filter, params)).toEqual('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#generateCustomAttributesInputType', () => {
|
||||
it('should return string if type is text', () => {
|
||||
expect(generateCustomAttributesInputType('text')).toEqual('string');
|
||||
});
|
||||
|
||||
it('should return string if type is number', () => {
|
||||
expect(generateCustomAttributesInputType('number')).toEqual('string');
|
||||
});
|
||||
|
||||
it('should return string if type is date', () => {
|
||||
expect(generateCustomAttributesInputType('date')).toEqual('string');
|
||||
});
|
||||
|
||||
it('should return multi_select if type is checkbox', () => {
|
||||
expect(generateCustomAttributesInputType('checkbox')).toEqual(
|
||||
'multi_select'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return multi_select if type is list', () => {
|
||||
expect(generateCustomAttributesInputType('list')).toEqual('multi_select');
|
||||
});
|
||||
|
||||
it('should return string if type is link', () => {
|
||||
expect(generateCustomAttributesInputType('link')).toEqual('string');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { generateFileName } from '../downloadHelper';
|
||||
|
||||
describe('#generateFileName', () => {
|
||||
it('should generate the correct file name', () => {
|
||||
expect(generateFileName({ type: 'csat', to: 1652812199 })).toEqual(
|
||||
'csat-report-17-05-2022.csv'
|
||||
);
|
||||
|
||||
expect(
|
||||
generateFileName({ type: 'csat', to: 1652812199, businessHours: true })
|
||||
).toEqual('csat-report-17-05-2022-business-hours.csv');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
// Moved from editorHelper.spec.js to editorContentHelper.spec.js
|
||||
// the mock of chatwoot/prosemirror-schema is getting conflicted with other specs
|
||||
import { getContentNode } from '../editorHelper';
|
||||
import { MessageMarkdownTransformer } from '@chatwoot/prosemirror-schema';
|
||||
import { replaceVariablesInMessage } from '@chatwoot/utils';
|
||||
|
||||
vi.mock('@chatwoot/prosemirror-schema', () => ({
|
||||
MessageMarkdownTransformer: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@chatwoot/utils', () => ({
|
||||
replaceVariablesInMessage: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('getContentNode', () => {
|
||||
let editorView;
|
||||
|
||||
beforeEach(() => {
|
||||
editorView = {
|
||||
state: {
|
||||
schema: {
|
||||
nodes: {
|
||||
mention: {
|
||||
create: vi.fn(),
|
||||
},
|
||||
},
|
||||
text: vi.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('getMentionNode', () => {
|
||||
it('should create a mention node', () => {
|
||||
const content = { id: 1, name: 'John Doe' };
|
||||
const from = 0;
|
||||
const to = 10;
|
||||
getContentNode(editorView, 'mention', content, {
|
||||
from,
|
||||
to,
|
||||
});
|
||||
|
||||
expect(editorView.state.schema.nodes.mention.create).toHaveBeenCalledWith(
|
||||
{
|
||||
userId: content.id,
|
||||
userFullName: content.name,
|
||||
mentionType: 'user',
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCannedResponseNode', () => {
|
||||
it('should create a canned response node', () => {
|
||||
const content = 'Hello {{name}}';
|
||||
const variables = { name: 'John' };
|
||||
const from = 0;
|
||||
const to = 10;
|
||||
const updatedMessage = 'Hello John';
|
||||
|
||||
// Mock the node that will be returned by parse
|
||||
const mockNode = { textContent: updatedMessage };
|
||||
|
||||
replaceVariablesInMessage.mockReturnValue(updatedMessage);
|
||||
|
||||
// Mock MessageMarkdownTransformer instance with parse method
|
||||
const mockTransformer = {
|
||||
parse: vi.fn().mockReturnValue(mockNode),
|
||||
};
|
||||
MessageMarkdownTransformer.mockImplementation(() => mockTransformer);
|
||||
|
||||
const result = getContentNode(
|
||||
editorView,
|
||||
'cannedResponse',
|
||||
content,
|
||||
{ from, to },
|
||||
variables
|
||||
);
|
||||
|
||||
expect(replaceVariablesInMessage).toHaveBeenCalledWith({
|
||||
message: content,
|
||||
variables,
|
||||
});
|
||||
expect(MessageMarkdownTransformer).toHaveBeenCalledWith(
|
||||
editorView.state.schema
|
||||
);
|
||||
expect(mockTransformer.parse).toHaveBeenCalledWith(updatedMessage);
|
||||
expect(result.node).toBe(mockNode);
|
||||
expect(result.node.textContent).toBe(updatedMessage);
|
||||
// When textContent matches updatedMessage, from should remain unchanged
|
||||
expect(result.from).toBe(from);
|
||||
expect(result.to).toBe(to);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVariableNode', () => {
|
||||
it('should create a variable node', () => {
|
||||
const content = 'name';
|
||||
const from = 0;
|
||||
const to = 10;
|
||||
getContentNode(editorView, 'variable', content, {
|
||||
from,
|
||||
to,
|
||||
});
|
||||
|
||||
expect(editorView.state.schema.text).toHaveBeenCalledWith('{{name}}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEmojiNode', () => {
|
||||
it('should create an emoji node', () => {
|
||||
const content = '😊';
|
||||
const from = 0;
|
||||
const to = 2;
|
||||
getContentNode(editorView, 'emoji', content, {
|
||||
from,
|
||||
to,
|
||||
});
|
||||
|
||||
expect(editorView.state.schema.text).toHaveBeenCalledWith('😊');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getContentNode', () => {
|
||||
it('should return null for invalid type', () => {
|
||||
const content = 'invalid';
|
||||
const from = 0;
|
||||
const to = 10;
|
||||
const { node } = getContentNode(editorView, 'invalid', content, {
|
||||
from,
|
||||
to,
|
||||
});
|
||||
|
||||
expect(node).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,153 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { EmailQuoteExtractor } from '../emailQuoteExtractor.js';
|
||||
|
||||
const SAMPLE_EMAIL_HTML = `
|
||||
<p>method</p>
|
||||
<blockquote>
|
||||
<p>On Mon, Sep 29, 2025 at 5:18 PM John <a href="mailto:shivam@chatwoot.com">shivam@chatwoot.com</a> wrote:</p>
|
||||
<p>Hi</p>
|
||||
<blockquote>
|
||||
<p>On Mon, Sep 29, 2025 at 5:17 PM Shivam Mishra <a href="mailto:shivam@chatwoot.com">shivam@chatwoot.com</a> wrote:</p>
|
||||
<p>Yes, it is.</p>
|
||||
<p>On Mon, Sep 29, 2025 at 5:16 PM John from Shaneforwoot < shaneforwoot@gmail.com> wrote:</p>
|
||||
<blockquote>
|
||||
<p>Hey</p>
|
||||
<p>On Mon, Sep 29, 2025 at 4:59 PM John shivam@chatwoot.com wrote:</p>
|
||||
<p>This is another quoted quoted text reply</p>
|
||||
<p>This is nice</p>
|
||||
<p>On Mon, Sep 29, 2025 at 4:21 PM John from Shaneforwoot < > shaneforwoot@gmail.com> wrote:</p>
|
||||
<p>Hey there, this is a reply from Chatwoot, notice the quoted text</p>
|
||||
<p>Hey there</p>
|
||||
<p>This is an email text, enjoy reading this</p>
|
||||
<p>-- Shivam Mishra, Chatwoot</p>
|
||||
</blockquote>
|
||||
</blockquote>
|
||||
</blockquote>
|
||||
`;
|
||||
|
||||
const EMAIL_WITH_SIGNATURE = `
|
||||
<p>Latest reply here.</p>
|
||||
<p>Thanks,</p>
|
||||
<p>Jane Doe</p>
|
||||
<blockquote>
|
||||
<p>On Mon, Sep 22, Someone wrote:</p>
|
||||
<p>Previous reply content</p>
|
||||
</blockquote>
|
||||
`;
|
||||
|
||||
const EMAIL_WITH_FOLLOW_UP_CONTENT = `
|
||||
<blockquote>
|
||||
<p>Inline quote that should stay</p>
|
||||
</blockquote>
|
||||
<p>Internal note follows</p>
|
||||
<p>Regards,</p>
|
||||
`;
|
||||
|
||||
describe('EmailQuoteExtractor', () => {
|
||||
it('removes blockquote-based quotes from the email body', () => {
|
||||
const cleanedHtml = EmailQuoteExtractor.extractQuotes(SAMPLE_EMAIL_HTML);
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = cleanedHtml;
|
||||
|
||||
expect(container.querySelectorAll('blockquote').length).toBe(0);
|
||||
expect(container.textContent?.trim()).toBe('method');
|
||||
expect(container.textContent).not.toContain(
|
||||
'On Mon, Sep 29, 2025 at 5:18 PM'
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps blockquote fallback when it is not the last top-level element', () => {
|
||||
const cleanedHtml = EmailQuoteExtractor.extractQuotes(
|
||||
EMAIL_WITH_FOLLOW_UP_CONTENT
|
||||
);
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = cleanedHtml;
|
||||
|
||||
expect(container.querySelector('blockquote')).not.toBeNull();
|
||||
expect(container.lastElementChild?.tagName).toBe('P');
|
||||
});
|
||||
|
||||
it('detects quote indicators in nested blockquotes', () => {
|
||||
const result = EmailQuoteExtractor.hasQuotes(SAMPLE_EMAIL_HTML);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('does not flag blockquotes that are followed by other elements', () => {
|
||||
expect(EmailQuoteExtractor.hasQuotes(EMAIL_WITH_FOLLOW_UP_CONTENT)).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('returns false when no quote indicators are present', () => {
|
||||
const html = '<p>Plain content</p>';
|
||||
expect(EmailQuoteExtractor.hasQuotes(html)).toBe(false);
|
||||
});
|
||||
|
||||
it('removes trailing blockquotes while preserving trailing signatures', () => {
|
||||
const cleanedHtml = EmailQuoteExtractor.extractQuotes(EMAIL_WITH_SIGNATURE);
|
||||
|
||||
expect(cleanedHtml).toContain('<p>Thanks,</p>');
|
||||
expect(cleanedHtml).toContain('<p>Jane Doe</p>');
|
||||
expect(cleanedHtml).not.toContain('<blockquote');
|
||||
});
|
||||
|
||||
it('detects quotes for trailing blockquotes even when signatures follow text', () => {
|
||||
expect(EmailQuoteExtractor.hasQuotes(EMAIL_WITH_SIGNATURE)).toBe(true);
|
||||
});
|
||||
|
||||
describe('HTML sanitization', () => {
|
||||
it('removes onerror handlers from img tags in extractQuotes', () => {
|
||||
const maliciousHtml = '<p>Hello</p><img src="x" onerror="alert(1)">';
|
||||
const cleanedHtml = EmailQuoteExtractor.extractQuotes(maliciousHtml);
|
||||
|
||||
expect(cleanedHtml).not.toContain('onerror');
|
||||
expect(cleanedHtml).toContain('<p>Hello</p>');
|
||||
});
|
||||
|
||||
it('removes onerror handlers from img tags in hasQuotes', () => {
|
||||
const maliciousHtml = '<p>Hello</p><img src="x" onerror="alert(1)">';
|
||||
// Should not throw and should safely check for quotes
|
||||
const result = EmailQuoteExtractor.hasQuotes(maliciousHtml);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('removes script tags in extractQuotes', () => {
|
||||
const maliciousHtml =
|
||||
'<p>Content</p><script>alert("xss")</script><p>More</p>';
|
||||
const cleanedHtml = EmailQuoteExtractor.extractQuotes(maliciousHtml);
|
||||
|
||||
expect(cleanedHtml).not.toContain('<script');
|
||||
expect(cleanedHtml).not.toContain('alert');
|
||||
expect(cleanedHtml).toContain('<p>Content</p>');
|
||||
expect(cleanedHtml).toContain('<p>More</p>');
|
||||
});
|
||||
|
||||
it('removes onclick handlers in extractQuotes', () => {
|
||||
const maliciousHtml = '<p onclick="alert(1)">Click me</p>';
|
||||
const cleanedHtml = EmailQuoteExtractor.extractQuotes(maliciousHtml);
|
||||
|
||||
expect(cleanedHtml).not.toContain('onclick');
|
||||
expect(cleanedHtml).toContain('Click me');
|
||||
});
|
||||
|
||||
it('removes javascript: URLs in extractQuotes', () => {
|
||||
const maliciousHtml = '<a href="javascript:alert(1)">Link</a>';
|
||||
const cleanedHtml = EmailQuoteExtractor.extractQuotes(maliciousHtml);
|
||||
|
||||
// eslint-disable-next-line no-script-url
|
||||
expect(cleanedHtml).not.toContain('javascript:');
|
||||
expect(cleanedHtml).toContain('Link');
|
||||
});
|
||||
|
||||
it('removes encoded payloads with event handlers in extractQuotes', () => {
|
||||
const maliciousHtml =
|
||||
'<img src="x" id="PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==" onerror="eval(atob(this.id))">';
|
||||
const cleanedHtml = EmailQuoteExtractor.extractQuotes(maliciousHtml);
|
||||
|
||||
expect(cleanedHtml).not.toContain('onerror');
|
||||
expect(cleanedHtml).not.toContain('eval');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import filterQueryGenerator from '../filterQueryGenerator';
|
||||
|
||||
const testData = [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: [
|
||||
{ id: 'pending', name: 'Pending' },
|
||||
{ id: 'resolved', name: 'Resolved' },
|
||||
],
|
||||
query_operator: 'and',
|
||||
},
|
||||
{
|
||||
attribute_key: 'assignee',
|
||||
filter_operator: 'equal_to',
|
||||
values: {
|
||||
id: 3,
|
||||
account_id: 1,
|
||||
auto_offline: true,
|
||||
confirmed: true,
|
||||
email: 'fayaz@test.com',
|
||||
available_name: 'Fayaz',
|
||||
name: 'Fayaz',
|
||||
role: 'agent',
|
||||
thumbnail:
|
||||
'https://www.gravatar.com/avatar/a35bf18a632f734c8d0c883dcc9fa0ef?d=404',
|
||||
},
|
||||
query_operator: 'and',
|
||||
},
|
||||
{
|
||||
attribute_key: 'id',
|
||||
filter_operator: 'equal_to',
|
||||
values: 'This is a test',
|
||||
query_operator: 'or',
|
||||
},
|
||||
];
|
||||
|
||||
const finalResult = {
|
||||
payload: [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: ['pending', 'resolved'],
|
||||
query_operator: 'and',
|
||||
},
|
||||
{
|
||||
attribute_key: 'assignee',
|
||||
filter_operator: 'equal_to',
|
||||
values: [3],
|
||||
query_operator: 'and',
|
||||
},
|
||||
{
|
||||
attribute_key: 'id',
|
||||
filter_operator: 'equal_to',
|
||||
values: ['This is a test'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('#filterQueryGenerator', () => {
|
||||
it('returns the correct format of filter query', () => {
|
||||
expect(filterQueryGenerator(testData)).toMatchObject(finalResult);
|
||||
expect(
|
||||
filterQueryGenerator(testData).payload.every(i => Array.isArray(i.values))
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
184
research/chatwoot/app/javascript/dashboard/helper/specs/fixtures/agentFixtures.js
vendored
Normal file
184
research/chatwoot/app/javascript/dashboard/helper/specs/fixtures/agentFixtures.js
vendored
Normal file
@@ -0,0 +1,184 @@
|
||||
export const allAgentsData = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'John K',
|
||||
confirmed: true,
|
||||
email: 'john@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'John Kennady',
|
||||
role: 'administrator',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Samuel K',
|
||||
confirmed: true,
|
||||
email: 'samuel@chatwoot.com',
|
||||
id: 2,
|
||||
name: 'Samuel Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
available_name: 'James K',
|
||||
confirmed: true,
|
||||
email: 'james@chatwoot.com',
|
||||
id: 3,
|
||||
name: 'James Koti',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Honey',
|
||||
confirmed: true,
|
||||
email: 'bee@chatwoot.com',
|
||||
id: 4,
|
||||
name: 'Honey Bee',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abraham@chatwoot.com',
|
||||
id: 5,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
export const onlineAgentsData = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abraham@chatwoot.com',
|
||||
id: 5,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'John K',
|
||||
confirmed: true,
|
||||
email: 'john@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'John Kennady',
|
||||
role: 'administrator',
|
||||
},
|
||||
];
|
||||
export const busyAgentsData = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Honey',
|
||||
confirmed: true,
|
||||
email: 'bee@chatwoot.com',
|
||||
id: 4,
|
||||
name: 'Honey Bee',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Samuel K',
|
||||
confirmed: true,
|
||||
email: 'samuel@chatwoot.com',
|
||||
id: 2,
|
||||
name: 'Samuel Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
export const offlineAgentsData = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
available_name: 'James K',
|
||||
confirmed: true,
|
||||
email: 'james@chatwoot.com',
|
||||
id: 3,
|
||||
name: 'James Koti',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
export const sortedByAvailability = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abraham@chatwoot.com',
|
||||
id: 5,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'John K',
|
||||
confirmed: true,
|
||||
email: 'john@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'John Kennady',
|
||||
role: 'administrator',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Honey',
|
||||
confirmed: true,
|
||||
email: 'bee@chatwoot.com',
|
||||
id: 4,
|
||||
name: 'Honey Bee',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Samuel K',
|
||||
confirmed: true,
|
||||
email: 'samuel@chatwoot.com',
|
||||
id: 2,
|
||||
name: 'Samuel Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
available_name: 'James K',
|
||||
confirmed: true,
|
||||
email: 'james@chatwoot.com',
|
||||
id: 3,
|
||||
name: 'James Koti',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
export const formattedAgentsByPresenceOnline = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abr@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
export const formattedAgentsByPresenceOffline = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abr@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
811
research/chatwoot/app/javascript/dashboard/helper/specs/fixtures/automationFixtures.js
vendored
Normal file
811
research/chatwoot/app/javascript/dashboard/helper/specs/fixtures/automationFixtures.js
vendored
Normal file
@@ -0,0 +1,811 @@
|
||||
import allLanguages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
|
||||
import allCountries from 'shared/constants/countries.js';
|
||||
|
||||
import {
|
||||
MESSAGE_CONDITION_VALUES,
|
||||
PRIORITY_CONDITION_VALUES,
|
||||
} from 'dashboard/constants/automation';
|
||||
|
||||
export const customAttributes = [
|
||||
{
|
||||
id: 1,
|
||||
attribute_display_name: 'Signed Up At',
|
||||
attribute_display_type: 'date',
|
||||
attribute_description: 'This is a test',
|
||||
attribute_key: 'signed_up_at',
|
||||
attribute_values: [],
|
||||
attribute_model: 'conversation_attribute',
|
||||
default_value: null,
|
||||
created_at: '2022-01-26T08:06:39.470Z',
|
||||
updated_at: '2022-01-26T08:06:39.470Z',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
attribute_display_name: 'Prime User',
|
||||
attribute_display_type: 'checkbox',
|
||||
attribute_description: 'Test',
|
||||
attribute_key: 'prime_user',
|
||||
attribute_values: [],
|
||||
attribute_model: 'contact_attribute',
|
||||
default_value: null,
|
||||
created_at: '2022-01-26T08:07:29.664Z',
|
||||
updated_at: '2022-01-26T08:07:29.664Z',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
attribute_display_name: 'Test',
|
||||
attribute_display_type: 'text',
|
||||
attribute_description: 'Test',
|
||||
attribute_key: 'test',
|
||||
attribute_values: [],
|
||||
attribute_model: 'conversation_attribute',
|
||||
default_value: null,
|
||||
created_at: '2022-01-26T08:07:58.325Z',
|
||||
updated_at: '2022-01-26T08:07:58.325Z',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
attribute_display_name: 'Link',
|
||||
attribute_display_type: 'link',
|
||||
attribute_description: 'Test',
|
||||
attribute_key: 'link',
|
||||
attribute_values: [],
|
||||
attribute_model: 'conversation_attribute',
|
||||
default_value: null,
|
||||
created_at: '2022-02-07T07:31:51.562Z',
|
||||
updated_at: '2022-02-07T07:31:51.562Z',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
attribute_display_name: 'My List',
|
||||
attribute_display_type: 'list',
|
||||
attribute_description: 'This is a sample list',
|
||||
attribute_key: 'my_list',
|
||||
attribute_values: ['item1', 'item2', 'item3'],
|
||||
attribute_model: 'conversation_attribute',
|
||||
default_value: null,
|
||||
created_at: '2022-02-21T20:31:34.175Z',
|
||||
updated_at: '2022-02-21T20:31:34.175Z',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
attribute_display_name: 'My Check',
|
||||
attribute_display_type: 'checkbox',
|
||||
attribute_description: 'Test Checkbox',
|
||||
attribute_key: 'my_check',
|
||||
attribute_values: [],
|
||||
attribute_model: 'conversation_attribute',
|
||||
default_value: null,
|
||||
created_at: '2022-02-21T20:31:53.385Z',
|
||||
updated_at: '2022-02-21T20:31:53.385Z',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
attribute_display_name: 'ConList',
|
||||
attribute_display_type: 'list',
|
||||
attribute_description: 'This is a test list\n',
|
||||
attribute_key: 'conlist',
|
||||
attribute_values: ['Hello', 'Test', 'Test2'],
|
||||
attribute_model: 'contact_attribute',
|
||||
default_value: null,
|
||||
created_at: '2022-02-28T12:58:05.005Z',
|
||||
updated_at: '2022-02-28T12:58:05.005Z',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
attribute_display_name: 'asdf',
|
||||
attribute_display_type: 'link',
|
||||
attribute_description: 'This is a some text',
|
||||
attribute_key: 'asdf',
|
||||
attribute_values: [],
|
||||
attribute_model: 'contact_attribute',
|
||||
default_value: null,
|
||||
created_at: '2022-04-21T05:48:16.168Z',
|
||||
updated_at: '2022-04-21T05:48:16.168Z',
|
||||
},
|
||||
];
|
||||
export const emptyAutomation = {
|
||||
name: null,
|
||||
description: null,
|
||||
event_name: 'conversation_created',
|
||||
conditions: [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
action_name: 'assign_team',
|
||||
action_params: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
export const filterAttributes = [
|
||||
{
|
||||
key: 'status',
|
||||
name: 'Status',
|
||||
attributeI18nKey: 'STATUS',
|
||||
inputType: 'multi_select',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'browser_language',
|
||||
name: 'Browser Language',
|
||||
attributeI18nKey: 'BROWSER_LANGUAGE',
|
||||
inputType: 'search_select',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'country_code',
|
||||
name: 'Country',
|
||||
attributeI18nKey: 'COUNTRY_NAME',
|
||||
inputType: 'search_select',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'referer',
|
||||
name: 'Referrer Link',
|
||||
attributeI18nKey: 'REFERER_LINK',
|
||||
inputType: 'plain_text',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
{ value: 'contains', label: 'Contains' },
|
||||
{ value: 'does_not_contain', label: 'Does not contain' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'inbox_id',
|
||||
name: 'Inbox',
|
||||
attributeI18nKey: 'INBOX',
|
||||
inputType: 'multi_select',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'conversation_custom_attribute',
|
||||
name: 'Conversation Custom Attributes',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: 'signed_up_at',
|
||||
name: 'Signed Up At',
|
||||
inputType: 'date',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
{ value: 'is_present', label: 'Is present' },
|
||||
{ value: 'is_not_present', label: 'Is not present' },
|
||||
{ value: 'is_greater_than', label: 'Is greater than' },
|
||||
{ value: 'is_less_than', label: 'Is less than' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'test',
|
||||
name: 'Test',
|
||||
inputType: 'plain_text',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
{ value: 'is_present', label: 'Is present' },
|
||||
{ value: 'is_not_present', label: 'Is not present' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'link',
|
||||
name: 'Link',
|
||||
inputType: 'plain_text',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'my_list',
|
||||
name: 'My List',
|
||||
inputType: 'search_select',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'my_check',
|
||||
name: 'My Check',
|
||||
inputType: 'search_select',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'contact_custom_attribute',
|
||||
name: 'Contact Custom Attributes',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: 'prime_user',
|
||||
name: 'Prime User',
|
||||
inputType: 'search_select',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'conlist',
|
||||
name: 'ConList',
|
||||
inputType: 'search_select',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'asdf',
|
||||
name: 'asdf',
|
||||
inputType: 'plain_text',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
],
|
||||
},
|
||||
];
|
||||
export const automation = {
|
||||
id: 164,
|
||||
account_id: 1,
|
||||
name: 'Attachment',
|
||||
description: 'Yo',
|
||||
event_name: 'conversation_created',
|
||||
conditions: [
|
||||
{
|
||||
values: [{ id: 'open', name: 'Open' }],
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
query_operator: 'and',
|
||||
},
|
||||
],
|
||||
actions: [{ action_name: 'send_attachment', action_params: [59] }],
|
||||
created_on: 1652717181,
|
||||
active: true,
|
||||
files: [
|
||||
{
|
||||
id: 50,
|
||||
automation_rule_id: 164,
|
||||
file_type: 'image/jpeg',
|
||||
account_id: 1,
|
||||
file_url:
|
||||
'http://localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBRQT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--965b4c27f4c5e47c526f0f38266b25417b72e5dd/pfp.jpeg',
|
||||
blob_id: 59,
|
||||
filename: 'pfp.jpeg',
|
||||
},
|
||||
],
|
||||
};
|
||||
export const agents = [
|
||||
{
|
||||
id: 1,
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
auto_offline: true,
|
||||
confirmed: true,
|
||||
email: 'john@acme.inc',
|
||||
available_name: 'Fayaz',
|
||||
name: 'Fayaz',
|
||||
role: 'administrator',
|
||||
thumbnail:
|
||||
'https://www.gravatar.com/avatar/0d722ac7bc3b3c92c030d0da9690d981?d=404',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
auto_offline: true,
|
||||
confirmed: true,
|
||||
email: 'john@doe.com',
|
||||
available_name: 'John',
|
||||
name: 'John',
|
||||
role: 'agent',
|
||||
thumbnail:
|
||||
'https://www.gravatar.com/avatar/6a6c19fea4a3676970167ce51f39e6ee?d=404',
|
||||
},
|
||||
];
|
||||
export const booleanFilterOptions = [
|
||||
{
|
||||
id: true,
|
||||
name: 'True',
|
||||
},
|
||||
{
|
||||
id: false,
|
||||
name: 'False',
|
||||
},
|
||||
];
|
||||
export const teams = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'sales team',
|
||||
description: 'This is our internal sales team',
|
||||
allow_auto_assign: true,
|
||||
account_id: 1,
|
||||
is_member: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'fayaz',
|
||||
description: 'Test',
|
||||
allow_auto_assign: true,
|
||||
account_id: 1,
|
||||
is_member: false,
|
||||
},
|
||||
];
|
||||
export const campaigns = [];
|
||||
export const contacts = [
|
||||
{
|
||||
additional_attributes: {},
|
||||
availability_status: 'offline',
|
||||
email: 'asd123123@asd.com',
|
||||
id: 32,
|
||||
name: 'asd123123',
|
||||
phone_number: null,
|
||||
identifier: null,
|
||||
thumbnail:
|
||||
'https://www.gravatar.com/avatar/46000d9a1eef3e24a02ca9d6c2a8f494?d=404',
|
||||
custom_attributes: {},
|
||||
conversations_count: 5,
|
||||
last_activity_at: 1650519706,
|
||||
},
|
||||
{
|
||||
additional_attributes: {},
|
||||
availability_status: 'offline',
|
||||
email: 'barry_allen@a.com',
|
||||
id: 29,
|
||||
name: 'barry_allen',
|
||||
phone_number: null,
|
||||
identifier: null,
|
||||
thumbnail:
|
||||
'https://www.gravatar.com/avatar/ab5ff99efa3bc1f74db1dc2885f9e2ce?d=404',
|
||||
custom_attributes: {},
|
||||
conversations_count: 1,
|
||||
last_activity_at: 1643728899,
|
||||
},
|
||||
];
|
||||
export const inboxes = [
|
||||
{
|
||||
id: 1,
|
||||
avatar_url: '',
|
||||
channel_id: 1,
|
||||
name: 'Acme Support',
|
||||
channel_type: 'Channel::WebWidget',
|
||||
greeting_enabled: false,
|
||||
greeting_message: '',
|
||||
working_hours_enabled: false,
|
||||
enable_email_collect: true,
|
||||
csat_survey_enabled: true,
|
||||
sender_name_type: 0,
|
||||
enable_auto_assignment: true,
|
||||
out_of_office_message:
|
||||
'We are unavailable at the moment. Leave a message we will respond once we are back.',
|
||||
working_hours: [
|
||||
{
|
||||
day_of_week: 0,
|
||||
closed_all_day: true,
|
||||
open_hour: null,
|
||||
open_minutes: null,
|
||||
close_hour: null,
|
||||
close_minutes: null,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 1,
|
||||
closed_all_day: false,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 2,
|
||||
closed_all_day: false,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 3,
|
||||
closed_all_day: false,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 4,
|
||||
closed_all_day: false,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 5,
|
||||
closed_all_day: false,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 6,
|
||||
closed_all_day: true,
|
||||
open_hour: null,
|
||||
open_minutes: null,
|
||||
close_hour: null,
|
||||
close_minutes: null,
|
||||
open_all_day: false,
|
||||
},
|
||||
],
|
||||
timezone: 'America/Los_Angeles',
|
||||
callback_webhook_url: null,
|
||||
allow_messages_after_resolved: true,
|
||||
widget_color: '#1f93ff',
|
||||
website_url: 'https://acme.inc',
|
||||
hmac_mandatory: false,
|
||||
welcome_title: '',
|
||||
welcome_tagline: '',
|
||||
web_widget_script:
|
||||
'\n <script>\n (function(d,t) {\n var BASE_URL="http://localhost:3000";\n var g=d.createElement(t),s=d.getElementsByTagName(t)[0];\n g.src=BASE_URL+"/packs/js/sdk.js";\n g.async = true;\n s.parentNode.insertBefore(g,s);\n g.onload=function(){\n window.chatwootSDK.run({\n websiteToken: \'yZ7USzaEs7hrwUAHLGwjbxJ1\',\n baseUrl: BASE_URL\n })\n }\n })(document,"script");\n </script>\n ',
|
||||
website_token: 'yZ7USzaEs7hrwUAHLGwjbxJ1',
|
||||
selected_feature_flags: ['attachments', 'emoji_picker', 'end_conversation'],
|
||||
reply_time: 'in_a_few_minutes',
|
||||
hmac_token: 'rRJW1BHu4aFMMey4SE7tWr8A',
|
||||
pre_chat_form_enabled: false,
|
||||
pre_chat_form_options: {
|
||||
pre_chat_fields: [
|
||||
{
|
||||
name: 'emailAddress',
|
||||
type: 'email',
|
||||
label: 'Email Id',
|
||||
enabled: false,
|
||||
required: true,
|
||||
field_type: 'standard',
|
||||
},
|
||||
{
|
||||
name: 'fullName',
|
||||
type: 'text',
|
||||
label: 'Full name',
|
||||
enabled: false,
|
||||
required: false,
|
||||
field_type: 'standard',
|
||||
},
|
||||
{
|
||||
name: 'phoneNumber',
|
||||
type: 'text',
|
||||
label: 'Phone number',
|
||||
enabled: false,
|
||||
required: false,
|
||||
field_type: 'standard',
|
||||
},
|
||||
],
|
||||
pre_chat_message: 'Share your queries or comments here.',
|
||||
},
|
||||
continuity_via_email: true,
|
||||
phone_number: null,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
avatar_url: '',
|
||||
channel_id: 1,
|
||||
name: 'Email',
|
||||
channel_type: 'Channel::Email',
|
||||
greeting_enabled: false,
|
||||
greeting_message: null,
|
||||
working_hours_enabled: false,
|
||||
enable_email_collect: true,
|
||||
csat_survey_enabled: false,
|
||||
enable_auto_assignment: true,
|
||||
out_of_office_message: null,
|
||||
working_hours: [
|
||||
{
|
||||
day_of_week: 0,
|
||||
closed_all_day: true,
|
||||
open_hour: null,
|
||||
open_minutes: null,
|
||||
close_hour: null,
|
||||
close_minutes: null,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 1,
|
||||
closed_all_day: false,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 2,
|
||||
closed_all_day: false,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 3,
|
||||
closed_all_day: false,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 4,
|
||||
closed_all_day: false,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 5,
|
||||
closed_all_day: false,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 6,
|
||||
closed_all_day: true,
|
||||
open_hour: null,
|
||||
open_minutes: null,
|
||||
close_hour: null,
|
||||
close_minutes: null,
|
||||
open_all_day: false,
|
||||
},
|
||||
],
|
||||
timezone: 'UTC',
|
||||
callback_webhook_url: null,
|
||||
allow_messages_after_resolved: true,
|
||||
widget_color: null,
|
||||
website_url: null,
|
||||
hmac_mandatory: null,
|
||||
welcome_title: null,
|
||||
welcome_tagline: null,
|
||||
web_widget_script: null,
|
||||
website_token: null,
|
||||
selected_feature_flags: null,
|
||||
reply_time: null,
|
||||
phone_number: null,
|
||||
forward_to_email: '9ae8ebb96c7f2d6705009f5add6d1a2d@false',
|
||||
email: 'fayaz@chatwoot.com',
|
||||
imap_login: '',
|
||||
imap_password: '',
|
||||
imap_address: '',
|
||||
imap_port: 0,
|
||||
imap_enabled: false,
|
||||
imap_enable_ssl: true,
|
||||
smtp_login: '',
|
||||
smtp_password: '',
|
||||
smtp_address: '',
|
||||
smtp_port: 0,
|
||||
smtp_enabled: false,
|
||||
smtp_domain: '',
|
||||
smtp_enable_ssl_tls: false,
|
||||
smtp_enable_starttls_auto: true,
|
||||
smtp_openssl_verify_mode: 'none',
|
||||
smtp_authentication: 'login',
|
||||
},
|
||||
];
|
||||
export const labels = [
|
||||
{
|
||||
id: 2,
|
||||
title: 'testlabel',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: 'snoozes',
|
||||
},
|
||||
];
|
||||
export const statusFilterOptions = [
|
||||
{ id: 'open', name: 'Open' },
|
||||
{ id: 'resolved', name: 'Resolved' },
|
||||
{ id: 'pending', name: 'Pending' },
|
||||
{ id: 'snoozed', name: 'Snoozed' },
|
||||
{ id: 'all', name: 'All' },
|
||||
];
|
||||
export const languages = allLanguages;
|
||||
export const countries = allCountries;
|
||||
|
||||
export const messageTypeOptions = MESSAGE_CONDITION_VALUES.map(item => ({
|
||||
id: item.id,
|
||||
name: `AUTOMATION.MESSAGE_TYPES.${item.i18nKey}`,
|
||||
}));
|
||||
|
||||
export const priorityOptions = PRIORITY_CONDITION_VALUES.map(item => ({
|
||||
id: item.id,
|
||||
name: `AUTOMATION.PRIORITY_TYPES.${item.i18nKey}`,
|
||||
}));
|
||||
|
||||
export const automationToSubmit = {
|
||||
name: 'Fayaz',
|
||||
description: 'Hello',
|
||||
event_name: 'conversation_created',
|
||||
conditions: [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: [{ id: 'open', name: 'Open' }],
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{ action_name: 'add_label', action_params: [{ id: 2, name: 'testlabel' }] },
|
||||
],
|
||||
};
|
||||
|
||||
export const savedAutomation = {
|
||||
id: 165,
|
||||
account_id: 1,
|
||||
name: 'Fayaz',
|
||||
description: 'Hello',
|
||||
event_name: 'conversation_created',
|
||||
conditions: [
|
||||
{
|
||||
values: ['open'],
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
action_name: 'add_label',
|
||||
action_params: [2],
|
||||
},
|
||||
],
|
||||
created_on: 1652776043,
|
||||
active: true,
|
||||
};
|
||||
|
||||
export const contactAttrs = [
|
||||
{
|
||||
key: 'contact_list',
|
||||
name: 'Contact List',
|
||||
inputType: 'search_select',
|
||||
filterOperators: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
export const conversationAttrs = [
|
||||
{
|
||||
key: 'text_attr',
|
||||
name: 'Text Attr',
|
||||
inputType: 'plain_text',
|
||||
filterOperators: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
{
|
||||
value: 'is_present',
|
||||
label: 'Is present',
|
||||
},
|
||||
{
|
||||
value: 'is_not_present',
|
||||
label: 'Is not present',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
export const expectedOutputForCustomAttributeGenerator = [
|
||||
{
|
||||
key: 'conversation_custom_attribute',
|
||||
name: 'Conversation Custom Attributes',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: 'text_attr',
|
||||
name: 'Text Attr',
|
||||
inputType: 'plain_text',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
{ value: 'is_present', label: 'Is present' },
|
||||
{ value: 'is_not_present', label: 'Is not present' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'contact_custom_attribute',
|
||||
name: 'Contact Custom Attributes',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: 'contact_list',
|
||||
name: 'Contact List',
|
||||
inputType: 'search_select',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
],
|
||||
},
|
||||
];
|
||||
export const slaPolicies = [
|
||||
{
|
||||
id: 1,
|
||||
account_id: 1,
|
||||
name: 'Low',
|
||||
first_response_time_threshold: 60,
|
||||
next_response_time_threshold: 120,
|
||||
resolution_time_threshold: 240,
|
||||
created_at: '2022-01-26T08:06:39.470Z',
|
||||
updated_at: '2022-01-26T08:06:39.470Z',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
account_id: 1,
|
||||
name: 'Medium',
|
||||
first_response_time_threshold: 30,
|
||||
next_response_time_threshold: 60,
|
||||
resolution_time_threshold: 120,
|
||||
created_at: '2022-01-26T08:06:39.470Z',
|
||||
updated_at: '2022-01-26T08:06:39.470Z',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
account_id: 1,
|
||||
name: 'High',
|
||||
first_response_time_threshold: 15,
|
||||
next_response_time_threshold: 30,
|
||||
resolution_time_threshold: 60,
|
||||
created_at: '2022-01-26T08:06:39.470Z',
|
||||
updated_at: '2022-01-26T08:06:39.470Z',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
account_id: 1,
|
||||
name: 'Urgent',
|
||||
first_response_time_threshold: 5,
|
||||
next_response_time_threshold: 10,
|
||||
resolution_time_threshold: 20,
|
||||
created_at: '2022-01-26T08:06:39.470Z',
|
||||
updated_at: '2022-01-26T08:06:39.470Z',
|
||||
},
|
||||
];
|
||||
185
research/chatwoot/app/javascript/dashboard/helper/specs/fixtures/conversationFixtures.js
vendored
Normal file
185
research/chatwoot/app/javascript/dashboard/helper/specs/fixtures/conversationFixtures.js
vendored
Normal file
@@ -0,0 +1,185 @@
|
||||
export const conversationData = {
|
||||
meta: {
|
||||
sender: {
|
||||
additional_attributes: {
|
||||
created_at_ip: '127.0.0.1',
|
||||
},
|
||||
availability_status: 'offline',
|
||||
email: null,
|
||||
id: 5017687,
|
||||
name: 'long-flower-143',
|
||||
phone_number: null,
|
||||
thumbnail: '',
|
||||
custom_attributes: {},
|
||||
},
|
||||
channel: 'Channel::WebWidget',
|
||||
assignee: {
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
confirmed: true,
|
||||
email: 'muhsin@chatwoot.com',
|
||||
available_name: 'Muhsin Keloth',
|
||||
id: 21,
|
||||
name: 'Muhsin Keloth',
|
||||
role: 'administrator',
|
||||
thumbnail: 'http://example.com/image.png',
|
||||
},
|
||||
},
|
||||
id: 5815,
|
||||
messages: [
|
||||
{
|
||||
id: 438072,
|
||||
content: 'Campaign after 5 seconds',
|
||||
account_id: 1,
|
||||
inbox_id: 37,
|
||||
conversation_id: 5811,
|
||||
message_type: 1,
|
||||
created_at: 1620980262,
|
||||
updated_at: '2021-05-14T08:17:42.041Z',
|
||||
private: false,
|
||||
status: 'sent',
|
||||
source_id: null,
|
||||
content_type: null,
|
||||
content_attributes: {},
|
||||
sender_type: 'User',
|
||||
sender_id: 1,
|
||||
external_source_ids: {},
|
||||
},
|
||||
{
|
||||
id: 4382131101,
|
||||
content: 'Hello',
|
||||
account_id: 1,
|
||||
inbox_id: 37,
|
||||
conversation_id: 5815,
|
||||
message_type: 0,
|
||||
created_at: 1621145476,
|
||||
updated_at: '2021-05-16T05:48:43.910Z',
|
||||
private: false,
|
||||
status: 'sent',
|
||||
source_id: null,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
sender_type: null,
|
||||
sender_id: null,
|
||||
external_source_ids: {},
|
||||
},
|
||||
{
|
||||
id: 438100,
|
||||
content: 'Hey',
|
||||
account_id: 1,
|
||||
inbox_id: 37,
|
||||
conversation_id: 5815,
|
||||
message_type: 0,
|
||||
created_at: 1621145476,
|
||||
updated_at: '2021-05-16T05:48:43.910Z',
|
||||
private: false,
|
||||
status: 'sent',
|
||||
source_id: null,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
sender_type: null,
|
||||
sender_id: null,
|
||||
external_source_ids: {},
|
||||
},
|
||||
],
|
||||
inbox_id: 37,
|
||||
status: 'open',
|
||||
muted: false,
|
||||
can_reply: true,
|
||||
timestamp: 1621144123,
|
||||
contact_last_seen_at: 0,
|
||||
agent_last_seen_at: 1621144123,
|
||||
unread_count: 0,
|
||||
additional_attributes: {
|
||||
browser: {
|
||||
device_name: 'Unknown',
|
||||
browser_name: 'Chrome',
|
||||
platform_name: 'macOS',
|
||||
browser_version: '90.0.4430.212',
|
||||
platform_version: '10.15.7',
|
||||
},
|
||||
widget_language: null,
|
||||
browser_language: 'en',
|
||||
},
|
||||
account_id: 1,
|
||||
labels: [],
|
||||
};
|
||||
|
||||
export const lastMessageData = {
|
||||
id: 438100,
|
||||
content: 'Hey',
|
||||
account_id: 1,
|
||||
inbox_id: 37,
|
||||
conversation_id: 5815,
|
||||
message_type: 0,
|
||||
created_at: 1621145476,
|
||||
updated_at: '2021-05-16T05:48:43.910Z',
|
||||
private: false,
|
||||
status: 'sent',
|
||||
source_id: null,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
sender_type: null,
|
||||
sender_id: null,
|
||||
external_source_ids: {},
|
||||
};
|
||||
|
||||
export const readMessagesData = [
|
||||
{
|
||||
id: 438072,
|
||||
content: 'Campaign after 5 seconds',
|
||||
account_id: 1,
|
||||
inbox_id: 37,
|
||||
conversation_id: 5811,
|
||||
message_type: 1,
|
||||
created_at: 1620980262,
|
||||
updated_at: '2021-05-14T08:17:42.041Z',
|
||||
private: false,
|
||||
status: 'sent',
|
||||
source_id: null,
|
||||
content_type: null,
|
||||
content_attributes: {},
|
||||
sender_type: 'User',
|
||||
sender_id: 1,
|
||||
external_source_ids: {},
|
||||
},
|
||||
];
|
||||
|
||||
export const unReadMessagesData = [
|
||||
{
|
||||
id: 4382131101,
|
||||
content: 'Hello',
|
||||
account_id: 1,
|
||||
inbox_id: 37,
|
||||
conversation_id: 5815,
|
||||
message_type: 0,
|
||||
created_at: 1621145476,
|
||||
updated_at: '2021-05-16T05:48:43.910Z',
|
||||
private: false,
|
||||
status: 'sent',
|
||||
source_id: null,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
sender_type: null,
|
||||
sender_id: null,
|
||||
external_source_ids: {},
|
||||
},
|
||||
{
|
||||
id: 438100,
|
||||
content: 'Hey',
|
||||
account_id: 1,
|
||||
inbox_id: 37,
|
||||
conversation_id: 5815,
|
||||
message_type: 0,
|
||||
created_at: 1621145476,
|
||||
updated_at: '2021-05-16T05:48:43.910Z',
|
||||
private: false,
|
||||
status: 'sent',
|
||||
source_id: null,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
sender_type: null,
|
||||
sender_id: null,
|
||||
external_source_ids: {},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,9 @@
|
||||
import { getCountryFlag } from '../flag';
|
||||
|
||||
describe('#flag', () => {
|
||||
it('returns the correct flag ', () => {
|
||||
expect(getCountryFlag('cz')).toBe('🇨🇿');
|
||||
expect(getCountryFlag('IN')).toBe('🇮🇳');
|
||||
expect(getCountryFlag('US')).toBe('🇺🇸');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,169 @@
|
||||
import {
|
||||
INBOX_TYPES,
|
||||
getInboxClassByType,
|
||||
getInboxIconByType,
|
||||
getInboxWarningIconClass,
|
||||
} from '../inbox';
|
||||
|
||||
describe('#Inbox Helpers', () => {
|
||||
describe('getInboxClassByType', () => {
|
||||
it('should return correct class for web widget', () => {
|
||||
expect(getInboxClassByType('Channel::WebWidget')).toEqual(
|
||||
'globe-desktop'
|
||||
);
|
||||
});
|
||||
it('should return correct class for fb page', () => {
|
||||
expect(getInboxClassByType('Channel::FacebookPage')).toEqual(
|
||||
'brand-facebook'
|
||||
);
|
||||
});
|
||||
it('should return correct class for twitter profile', () => {
|
||||
expect(getInboxClassByType('Channel::TwitterProfile')).toEqual(
|
||||
'brand-twitter'
|
||||
);
|
||||
});
|
||||
it('should return correct class for twilio sms', () => {
|
||||
expect(getInboxClassByType('Channel::TwilioSms', '')).toEqual(
|
||||
'brand-sms'
|
||||
);
|
||||
});
|
||||
it('should return correct class for whatsapp', () => {
|
||||
expect(getInboxClassByType('Channel::TwilioSms', 'whatsapp')).toEqual(
|
||||
'brand-whatsapp'
|
||||
);
|
||||
});
|
||||
it('should return correct class for Api', () => {
|
||||
expect(getInboxClassByType('Channel::Api')).toEqual('cloud');
|
||||
});
|
||||
it('should return correct class for Email', () => {
|
||||
expect(getInboxClassByType('Channel::Email')).toEqual('mail');
|
||||
});
|
||||
it('should return correct class for TikTok', () => {
|
||||
expect(getInboxClassByType(INBOX_TYPES.TIKTOK)).toEqual('brand-tiktok');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInboxIconByType', () => {
|
||||
describe('fill variant (default)', () => {
|
||||
it('returns correct icon for web widget', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.WEB)).toBe('i-ri-global-fill');
|
||||
});
|
||||
|
||||
it('returns correct icon for Facebook', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.FB)).toBe('i-ri-messenger-fill');
|
||||
});
|
||||
|
||||
it('returns correct icon for Twitter', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.TWITTER)).toBe(
|
||||
'i-ri-twitter-x-fill'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns correct icon for WhatsApp', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.WHATSAPP)).toBe(
|
||||
'i-ri-whatsapp-fill'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns correct icon for API', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.API)).toBe('i-ri-cloudy-fill');
|
||||
});
|
||||
|
||||
it('returns correct icon for Email', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.EMAIL)).toBe('i-ri-mail-fill');
|
||||
});
|
||||
|
||||
it('returns correct icon for Telegram', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.TELEGRAM)).toBe(
|
||||
'i-ri-telegram-fill'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns correct icon for Line', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.LINE)).toBe('i-ri-line-fill');
|
||||
});
|
||||
|
||||
it('returns correct icon for TikTok', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.TIKTOK)).toBe('i-ri-tiktok-fill');
|
||||
});
|
||||
|
||||
it('returns default icon for unknown type', () => {
|
||||
expect(getInboxIconByType('UNKNOWN_TYPE')).toBe('i-ri-chat-1-fill');
|
||||
});
|
||||
|
||||
it('returns default icon for undefined type', () => {
|
||||
expect(getInboxIconByType(undefined)).toBe('i-ri-chat-1-fill');
|
||||
});
|
||||
});
|
||||
|
||||
describe('line variant', () => {
|
||||
it('returns correct line icon for web widget', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.WEB, null, 'line')).toBe(
|
||||
'i-woot-website'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns correct line icon for Facebook', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.FB, null, 'line')).toBe(
|
||||
'i-woot-messenger'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns correct line icon for TikTok', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.TIKTOK, null, 'line')).toBe(
|
||||
'i-woot-tiktok'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns correct line icon for unknown type', () => {
|
||||
expect(getInboxIconByType('UNKNOWN_TYPE', null, 'line')).toBe(
|
||||
'i-ri-chat-1-line'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Twilio cases', () => {
|
||||
describe('fill variant', () => {
|
||||
it('returns WhatsApp icon for Twilio WhatsApp number', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.TWILIO, 'whatsapp')).toBe(
|
||||
'i-ri-whatsapp-fill'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns SMS icon for regular Twilio number', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.TWILIO, 'sms')).toBe(
|
||||
'i-ri-chat-1-fill'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns SMS icon when phone number is undefined', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.TWILIO, undefined)).toBe(
|
||||
'i-ri-chat-1-fill'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('line variant', () => {
|
||||
it('returns WhatsApp line icon for Twilio WhatsApp number', () => {
|
||||
expect(
|
||||
getInboxIconByType(INBOX_TYPES.TWILIO, 'whatsapp', 'line')
|
||||
).toBe('i-woot-whatsapp');
|
||||
});
|
||||
|
||||
it('returns SMS line icon for regular Twilio number', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.TWILIO, 'sms', 'line')).toBe(
|
||||
'i-ri-chat-1-line'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInboxWarningIconClass', () => {
|
||||
it('should return correct class for warning', () => {
|
||||
expect(getInboxWarningIconClass('Channel::FacebookPage', true)).toEqual(
|
||||
'warning'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
export default {
|
||||
customFields: {
|
||||
pre_chat_message: 'Share your queries or comments here.',
|
||||
pre_chat_fields: [
|
||||
{
|
||||
label: 'Email Address',
|
||||
name: 'emailAddress',
|
||||
type: 'email',
|
||||
field_type: 'standard',
|
||||
required: false,
|
||||
enabled: false,
|
||||
|
||||
placeholder: 'Please enter your email address',
|
||||
},
|
||||
{
|
||||
label: 'Full Name',
|
||||
name: 'fullName',
|
||||
type: 'text',
|
||||
field_type: 'standard',
|
||||
required: false,
|
||||
enabled: false,
|
||||
placeholder: 'Please enter your full name',
|
||||
},
|
||||
{
|
||||
label: 'Phone Number',
|
||||
name: 'phoneNumber',
|
||||
type: 'text',
|
||||
field_type: 'standard',
|
||||
required: false,
|
||||
enabled: false,
|
||||
placeholder: 'Please enter your phone number',
|
||||
},
|
||||
],
|
||||
},
|
||||
customAttributes: [
|
||||
{
|
||||
id: 101,
|
||||
attribute_description: 'Order Identifier',
|
||||
attribute_display_name: 'Order Id',
|
||||
attribute_display_type: 'number',
|
||||
attribute_key: 'order_id',
|
||||
attribute_model: 'conversation_attribute',
|
||||
attribute_values: Array(0),
|
||||
created_at: '2021-11-29T10:20:04.563Z',
|
||||
},
|
||||
],
|
||||
customAttributesWithRegex: [
|
||||
{
|
||||
id: 2,
|
||||
attribute_description: 'Test contact Attribute',
|
||||
attribute_display_name: 'Test contact Attribute',
|
||||
attribute_display_type: 'text',
|
||||
attribute_key: 'test_contact_attribute',
|
||||
attribute_model: 'contact_attribute',
|
||||
attribute_values: Array(0),
|
||||
created_at: '2023-09-20T10:20:04.563Z',
|
||||
regex_pattern: '^w+$',
|
||||
regex_cue: 'It should be a combination of alphabets and numbers',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
export const teams = [
|
||||
{
|
||||
id: 1,
|
||||
name: '⚙️ sales team',
|
||||
description: 'This is our internal sales team',
|
||||
allow_auto_assign: true,
|
||||
account_id: 1,
|
||||
is_member: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '🤷♂️ fayaz',
|
||||
description: 'Test',
|
||||
allow_auto_assign: true,
|
||||
account_id: 1,
|
||||
is_member: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '🇮🇳 apac sales',
|
||||
description: 'Sales team for France Territory',
|
||||
allow_auto_assign: true,
|
||||
account_id: 1,
|
||||
is_member: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const labels = [
|
||||
{
|
||||
id: 6,
|
||||
title: 'sales',
|
||||
description: 'sales team',
|
||||
color: '#8EA20F',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'billing',
|
||||
description: 'billing',
|
||||
color: '#4077DA',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: 'snoozed',
|
||||
description: 'Items marked for later',
|
||||
color: '#D12F42',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: 'mobile-app',
|
||||
description: 'tech team',
|
||||
color: '#2DB1CC',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
title: 'human-resources-department-with-long-title',
|
||||
description: 'Test',
|
||||
color: '#FF6E09',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
title: 'priority',
|
||||
description: 'For important sales leads',
|
||||
color: '#7E7CED',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const agents = [
|
||||
{
|
||||
id: 1,
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
auto_offline: true,
|
||||
confirmed: true,
|
||||
email: 'john@doe.com',
|
||||
available_name: 'John Doe',
|
||||
name: 'John Doe',
|
||||
role: 'agent',
|
||||
thumbnail:
|
||||
'http://localhost:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBUZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--746506837470c1a3dd063e90211ba2386963d52f/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2QzNKbGMybDZaVWtpRERJMU1IZ3lOVEFHT3daVSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--e0e35266e8ed66e90c51be02408be8a022aca545/batman_90804.png',
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
auto_offline: true,
|
||||
confirmed: true,
|
||||
email: 'clark@kent.com',
|
||||
available_name: 'Clark Kent',
|
||||
name: 'Clark Kent',
|
||||
role: 'agent',
|
||||
thumbnail: '',
|
||||
},
|
||||
];
|
||||
|
||||
export const files = [
|
||||
{
|
||||
id: 76,
|
||||
macro_id: 77,
|
||||
file_type: 'image/jpeg',
|
||||
account_id: 1,
|
||||
file_url:
|
||||
'http://localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBYUT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--aa41b5a779a83c1d86b28475a5cf0bd17f41f0ff/fayaz_cropped.jpeg',
|
||||
blob_id: 88,
|
||||
filename: 'fayaz_cropped.jpeg',
|
||||
},
|
||||
{
|
||||
id: 82,
|
||||
macro_id: 77,
|
||||
file_type: 'image/png',
|
||||
account_id: 1,
|
||||
file_url:
|
||||
'http://localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBZdz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--260fda80b77409ffaaac10b96681fba447600545/screenshot.png',
|
||||
blob_id: 94,
|
||||
filename: 'screenshot.png',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
emptyMacro,
|
||||
resolveActionName,
|
||||
resolveLabels,
|
||||
resolveTeamIds,
|
||||
getFileName,
|
||||
resolveAgents,
|
||||
} from '../../routes/dashboard/settings/macros/macroHelper';
|
||||
import { MACRO_ACTION_TYPES } from '../../routes/dashboard/settings/macros/constants';
|
||||
import { teams, labels, files, agents } from './macrosFixtures';
|
||||
|
||||
describe('#emptyMacro', () => {
|
||||
const defaultMacro = {
|
||||
name: '',
|
||||
actions: [
|
||||
{
|
||||
action_name: 'assign_team',
|
||||
action_params: [],
|
||||
},
|
||||
],
|
||||
visibility: 'global',
|
||||
};
|
||||
it('returns the default macro', () => {
|
||||
expect(emptyMacro).toEqual(defaultMacro);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#resolveActionName', () => {
|
||||
it('resolve action name from key and return the correct label', () => {
|
||||
expect(resolveActionName(MACRO_ACTION_TYPES[0].key)).toEqual(
|
||||
MACRO_ACTION_TYPES[0].label
|
||||
);
|
||||
expect(resolveActionName(MACRO_ACTION_TYPES[1].key)).toEqual(
|
||||
MACRO_ACTION_TYPES[1].label
|
||||
);
|
||||
expect(resolveActionName(MACRO_ACTION_TYPES[1].key)).not.toEqual(
|
||||
MACRO_ACTION_TYPES[0].label
|
||||
);
|
||||
expect(resolveActionName('change_priority')).toEqual('CHANGE_PRIORITY'); // Translated
|
||||
});
|
||||
});
|
||||
|
||||
describe('#resolveTeamIds', () => {
|
||||
it('resolves team names from ids, and returns a joined string', () => {
|
||||
const resolvedTeams = '⚙️ sales team, 🤷♂️ fayaz';
|
||||
expect(resolveTeamIds(teams, [1, 2])).toEqual(resolvedTeams);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#resolveLabels', () => {
|
||||
it('resolves labels names from ids and returns a joined string', () => {
|
||||
const resolvedLabels = 'sales, billing';
|
||||
expect(resolveLabels(labels, ['sales', 'billing'])).toEqual(resolvedLabels);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#resolveAgents', () => {
|
||||
it('resolves agents names from ids and returns a joined string', () => {
|
||||
const resolvedAgents = 'John Doe';
|
||||
expect(resolveAgents(agents, [1])).toEqual(resolvedAgents);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getFileName', () => {
|
||||
it('returns the correct file name from the list of files', () => {
|
||||
expect(getFileName(files[0].blob_id, 'send_attachment', files)).toEqual(
|
||||
files[0].filename
|
||||
);
|
||||
expect(getFileName(files[1].blob_id, 'send_attachment', files)).toEqual(
|
||||
files[1].filename
|
||||
);
|
||||
expect(getFileName(files[0].blob_id, 'wrong_action', files)).toEqual('');
|
||||
expect(getFileName(null, 'send_attachment', files)).toEqual('');
|
||||
expect(getFileName(files[0].blob_id, 'send_attachment', [])).toEqual('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
import {
|
||||
getCurrentAccount,
|
||||
getUserPermissions,
|
||||
hasPermissions,
|
||||
filterItemsByPermission,
|
||||
} from '../permissionsHelper';
|
||||
|
||||
describe('#getCurrentAccount', () => {
|
||||
it('should return the current account', () => {
|
||||
expect(getCurrentAccount({ accounts: [{ id: 1 }] }, 1)).toEqual({ id: 1 });
|
||||
expect(getCurrentAccount({ accounts: [] }, 1)).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getUserPermissions', () => {
|
||||
it('should return the correct permissions', () => {
|
||||
const user = {
|
||||
accounts: [
|
||||
{ id: 1, permissions: ['conversations_manage'] },
|
||||
{ id: 3, permissions: ['contacts_manage'] },
|
||||
],
|
||||
};
|
||||
expect(getUserPermissions(user, 1)).toEqual(['conversations_manage']);
|
||||
expect(getUserPermissions(user, '3')).toEqual(['contacts_manage']);
|
||||
expect(getUserPermissions(user, 2)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasPermissions', () => {
|
||||
it('returns true if permission is present', () => {
|
||||
expect(
|
||||
hasPermissions(['contact_manage'], ['team_manage', 'contact_manage'])
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true if permission is not present', () => {
|
||||
expect(
|
||||
hasPermissions(['contact_manage'], ['team_manage', 'user_manage'])
|
||||
).toBe(false);
|
||||
expect(hasPermissions()).toBe(false);
|
||||
expect(hasPermissions([])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterItemsByPermission', () => {
|
||||
const items = {
|
||||
item1: { name: 'Item 1', permissions: ['agent', 'administrator'] },
|
||||
item2: {
|
||||
name: 'Item 2',
|
||||
permissions: [
|
||||
'conversation_manage',
|
||||
'conversation_unassigned_manage',
|
||||
'conversation_participating_manage',
|
||||
],
|
||||
},
|
||||
item3: { name: 'Item 3', permissions: ['contact_manage'] },
|
||||
item4: { name: 'Item 4', permissions: ['report_manage'] },
|
||||
item5: { name: 'Item 5', permissions: ['knowledge_base_manage'] },
|
||||
item6: {
|
||||
name: 'Item 6',
|
||||
permissions: [
|
||||
'agent',
|
||||
'administrator',
|
||||
'conversation_manage',
|
||||
'conversation_unassigned_manage',
|
||||
'conversation_participating_manage',
|
||||
'contact_manage',
|
||||
'report_manage',
|
||||
'knowledge_base_manage',
|
||||
],
|
||||
},
|
||||
item7: { name: 'Item 7', permissions: [] },
|
||||
};
|
||||
|
||||
const getPermissions = item => item.permissions;
|
||||
|
||||
it('filters items based on user permissions', () => {
|
||||
const userPermissions = ['agent', 'contact_manage', 'report_manage'];
|
||||
const result = filterItemsByPermission(
|
||||
items,
|
||||
userPermissions,
|
||||
getPermissions
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(5);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'item1', name: 'Item 1' })
|
||||
);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'item3', name: 'Item 3' })
|
||||
);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'item4', name: 'Item 4' })
|
||||
);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'item6', name: 'Item 6' })
|
||||
);
|
||||
});
|
||||
|
||||
it('includes items with empty permissions', () => {
|
||||
const userPermissions = [];
|
||||
const result = filterItemsByPermission(
|
||||
items,
|
||||
userPermissions,
|
||||
getPermissions
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'item7', name: 'Item 7' })
|
||||
);
|
||||
});
|
||||
|
||||
it('uses custom transform function when provided', () => {
|
||||
const userPermissions = ['agent', 'contact_manage'];
|
||||
const customTransform = (key, item) => ({ id: key, title: item.name });
|
||||
const result = filterItemsByPermission(
|
||||
items,
|
||||
userPermissions,
|
||||
getPermissions,
|
||||
customTransform
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(4);
|
||||
expect(result).toContainEqual({ id: 'item1', title: 'Item 1' });
|
||||
expect(result).toContainEqual({ id: 'item3', title: 'Item 3' });
|
||||
expect(result).toContainEqual({ id: 'item6', title: 'Item 6' });
|
||||
});
|
||||
|
||||
it('handles empty items object', () => {
|
||||
const result = filterItemsByPermission({}, ['agent'], getPermissions);
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('handles custom getPermissions function', () => {
|
||||
const customItems = {
|
||||
item1: { name: 'Item 1', requiredPerms: ['agent', 'administrator'] },
|
||||
item2: { name: 'Item 2', requiredPerms: ['contact_manage'] },
|
||||
};
|
||||
const customGetPermissions = item => item.requiredPerms;
|
||||
const result = filterItemsByPermission(
|
||||
customItems,
|
||||
['agent'],
|
||||
customGetPermissions
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'item1', name: 'Item 1' })
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { buildPortalArticleURL, buildPortalURL } from '../portalHelper';
|
||||
|
||||
describe('PortalHelper', () => {
|
||||
describe('buildPortalURL', () => {
|
||||
it('returns the correct url', () => {
|
||||
window.chatwootConfig = {
|
||||
hostURL: 'https://app.chatwoot.com',
|
||||
helpCenterURL: 'https://help.chatwoot.com',
|
||||
};
|
||||
expect(buildPortalURL('handbook')).toEqual(
|
||||
'https://help.chatwoot.com/hc/handbook'
|
||||
);
|
||||
window.chatwootConfig = {};
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildPortalArticleURL', () => {
|
||||
it('returns the correct url', () => {
|
||||
window.chatwootConfig = {
|
||||
hostURL: 'https://app.chatwoot.com',
|
||||
helpCenterURL: 'https://help.chatwoot.com',
|
||||
};
|
||||
expect(
|
||||
buildPortalArticleURL('handbook', 'culture', 'fr', 'article-slug')
|
||||
).toEqual('https://help.chatwoot.com/hc/handbook/articles/article-slug');
|
||||
window.chatwootConfig = {};
|
||||
});
|
||||
|
||||
it('returns the correct url with custom domain', () => {
|
||||
window.chatwootConfig = {
|
||||
hostURL: 'https://app.chatwoot.com',
|
||||
helpCenterURL: 'https://help.chatwoot.com',
|
||||
};
|
||||
expect(
|
||||
buildPortalArticleURL(
|
||||
'handbook',
|
||||
'culture',
|
||||
'fr',
|
||||
'article-slug',
|
||||
'custom-domain.dev'
|
||||
)
|
||||
).toEqual('https://custom-domain.dev/hc/handbook/articles/article-slug');
|
||||
});
|
||||
|
||||
it('handles https in custom domain correctly', () => {
|
||||
window.chatwootConfig = {
|
||||
hostURL: 'https://app.chatwoot.com',
|
||||
helpCenterURL: 'https://help.chatwoot.com',
|
||||
};
|
||||
expect(
|
||||
buildPortalArticleURL(
|
||||
'handbook',
|
||||
'culture',
|
||||
'fr',
|
||||
'article-slug',
|
||||
'https://custom-domain.dev'
|
||||
)
|
||||
).toEqual('https://custom-domain.dev/hc/handbook/articles/article-slug');
|
||||
});
|
||||
|
||||
it('uses hostURL when helpCenterURL is not available', () => {
|
||||
window.chatwootConfig = {
|
||||
hostURL: 'https://app.chatwoot.com',
|
||||
helpCenterURL: '',
|
||||
};
|
||||
expect(
|
||||
buildPortalArticleURL('handbook', 'culture', 'fr', 'article-slug')
|
||||
).toEqual('https://app.chatwoot.com/hc/handbook/articles/article-slug');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
getPreChatFields,
|
||||
getFormattedPreChatFields,
|
||||
getCustomFields,
|
||||
} from '../preChat';
|
||||
import inboxFixture from './inboxFixture';
|
||||
|
||||
const { customFields, customAttributes, customAttributesWithRegex } =
|
||||
inboxFixture;
|
||||
describe('#Pre chat Helpers', () => {
|
||||
describe('getPreChatFields', () => {
|
||||
it('should return correct pre-chat fields form options passed', () => {
|
||||
expect(getPreChatFields({ preChatFormOptions: customFields })).toEqual(
|
||||
customFields
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('getFormattedPreChatFields', () => {
|
||||
it('should return correct custom fields', () => {
|
||||
expect(
|
||||
getFormattedPreChatFields({
|
||||
preChatFields: customFields.pre_chat_fields,
|
||||
})
|
||||
).toEqual([
|
||||
{
|
||||
label: 'Email Address',
|
||||
name: 'emailAddress',
|
||||
placeholder: 'Please enter your email address',
|
||||
type: 'email',
|
||||
field_type: 'standard',
|
||||
required: false,
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
label: 'Full Name',
|
||||
name: 'fullName',
|
||||
placeholder: 'Please enter your full name',
|
||||
type: 'text',
|
||||
field_type: 'standard',
|
||||
required: false,
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
label: 'Phone Number',
|
||||
name: 'phoneNumber',
|
||||
placeholder: 'Please enter your phone number',
|
||||
type: 'text',
|
||||
field_type: 'standard',
|
||||
required: false,
|
||||
enabled: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('getCustomFields', () => {
|
||||
it('should return correct custom fields', () => {
|
||||
expect(
|
||||
getCustomFields({
|
||||
standardFields: { pre_chat_fields: customFields.pre_chat_fields },
|
||||
customAttributes,
|
||||
})
|
||||
).toEqual([
|
||||
{
|
||||
enabled: false,
|
||||
label: 'Order Id',
|
||||
placeholder: 'Order Id',
|
||||
name: 'order_id',
|
||||
required: false,
|
||||
field_type: 'conversation_attribute',
|
||||
type: 'number',
|
||||
values: [],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(
|
||||
getCustomFields({
|
||||
standardFields: { pre_chat_fields: customFields.pre_chat_fields },
|
||||
customAttributes: customAttributesWithRegex,
|
||||
})
|
||||
).toEqual([
|
||||
{
|
||||
enabled: false,
|
||||
label: 'Test contact Attribute',
|
||||
placeholder: 'Test contact Attribute',
|
||||
name: 'test_contact_attribute',
|
||||
required: false,
|
||||
field_type: 'contact_attribute',
|
||||
type: 'text',
|
||||
values: [],
|
||||
regex_pattern: '^w+$',
|
||||
regex_cue: 'It should be a combination of alphabets and numbers',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,461 @@
|
||||
import {
|
||||
extractPlainTextFromHtml,
|
||||
getEmailSenderName,
|
||||
getEmailSenderEmail,
|
||||
getEmailDate,
|
||||
formatQuotedEmailDate,
|
||||
getInboxEmail,
|
||||
buildQuotedEmailHeader,
|
||||
buildQuotedEmailHeaderFromContact,
|
||||
buildQuotedEmailHeaderFromInbox,
|
||||
formatQuotedTextAsBlockquote,
|
||||
extractQuotedEmailText,
|
||||
truncatePreviewText,
|
||||
appendQuotedTextToMessage,
|
||||
} from '../quotedEmailHelper';
|
||||
|
||||
describe('quotedEmailHelper', () => {
|
||||
describe('extractPlainTextFromHtml', () => {
|
||||
it('returns empty string for null or undefined', () => {
|
||||
expect(extractPlainTextFromHtml(null)).toBe('');
|
||||
expect(extractPlainTextFromHtml(undefined)).toBe('');
|
||||
});
|
||||
|
||||
it('strips HTML tags and returns plain text', () => {
|
||||
const html = '<p>Hello <strong>world</strong></p>';
|
||||
const result = extractPlainTextFromHtml(html);
|
||||
expect(result).toBe('Hello world');
|
||||
});
|
||||
|
||||
it('handles complex HTML structure', () => {
|
||||
const html = '<div><p>Line 1</p><p>Line 2</p></div>';
|
||||
const result = extractPlainTextFromHtml(html);
|
||||
expect(result).toContain('Line 1');
|
||||
expect(result).toContain('Line 2');
|
||||
});
|
||||
|
||||
it('sanitizes onerror handlers from img tags', () => {
|
||||
const html = '<p>Hello</p><img src="x" onerror="alert(1)">';
|
||||
const result = extractPlainTextFromHtml(html);
|
||||
expect(result).toBe('Hello');
|
||||
});
|
||||
|
||||
it('sanitizes script tags', () => {
|
||||
const html = '<p>Safe</p><script>alert(1)</script><p>Content</p>';
|
||||
const result = extractPlainTextFromHtml(html);
|
||||
expect(result).toContain('Safe');
|
||||
expect(result).toContain('Content');
|
||||
expect(result).not.toContain('alert');
|
||||
});
|
||||
|
||||
it('sanitizes onclick handlers', () => {
|
||||
const html = '<p onclick="alert(1)">Click me</p>';
|
||||
const result = extractPlainTextFromHtml(html);
|
||||
expect(result).toBe('Click me');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEmailSenderName', () => {
|
||||
it('returns sender name from lastEmail', () => {
|
||||
const lastEmail = { sender: { name: 'John Doe' } };
|
||||
const result = getEmailSenderName(lastEmail, {});
|
||||
expect(result).toBe('John Doe');
|
||||
});
|
||||
|
||||
it('returns contact name if sender name not available', () => {
|
||||
const lastEmail = { sender: {} };
|
||||
const contact = { name: 'Jane Smith' };
|
||||
const result = getEmailSenderName(lastEmail, contact);
|
||||
expect(result).toBe('Jane Smith');
|
||||
});
|
||||
|
||||
it('returns empty string if neither available', () => {
|
||||
const result = getEmailSenderName({}, {});
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('trims whitespace from names', () => {
|
||||
const lastEmail = { sender: { name: ' John Doe ' } };
|
||||
const result = getEmailSenderName(lastEmail, {});
|
||||
expect(result).toBe('John Doe');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEmailSenderEmail', () => {
|
||||
it('returns sender email from lastEmail', () => {
|
||||
const lastEmail = { sender: { email: 'john@example.com' } };
|
||||
const result = getEmailSenderEmail(lastEmail, {});
|
||||
expect(result).toBe('john@example.com');
|
||||
});
|
||||
|
||||
it('returns email from contentAttributes if sender email not available', () => {
|
||||
const lastEmail = {
|
||||
contentAttributes: {
|
||||
email: { from: ['jane@example.com'] },
|
||||
},
|
||||
};
|
||||
const result = getEmailSenderEmail(lastEmail, {});
|
||||
expect(result).toBe('jane@example.com');
|
||||
});
|
||||
|
||||
it('returns contact email as fallback', () => {
|
||||
const lastEmail = {};
|
||||
const contact = { email: 'contact@example.com' };
|
||||
const result = getEmailSenderEmail(lastEmail, contact);
|
||||
expect(result).toBe('contact@example.com');
|
||||
});
|
||||
|
||||
it('trims whitespace from emails', () => {
|
||||
const lastEmail = { sender: { email: ' john@example.com ' } };
|
||||
const result = getEmailSenderEmail(lastEmail, {});
|
||||
expect(result).toBe('john@example.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEmailDate', () => {
|
||||
it('returns parsed date from email metadata', () => {
|
||||
const lastEmail = {
|
||||
contentAttributes: {
|
||||
email: { date: '2024-01-15T10:30:00Z' },
|
||||
},
|
||||
};
|
||||
const result = getEmailDate(lastEmail);
|
||||
expect(result).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('returns date from created_at timestamp', () => {
|
||||
const lastEmail = { created_at: 1705318200 };
|
||||
const result = getEmailDate(lastEmail);
|
||||
expect(result).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('handles millisecond timestamps', () => {
|
||||
const lastEmail = { created_at: 1705318200000 };
|
||||
const result = getEmailDate(lastEmail);
|
||||
expect(result).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('returns null if no valid date found', () => {
|
||||
const result = getEmailDate({});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatQuotedEmailDate', () => {
|
||||
it('formats date correctly', () => {
|
||||
const date = new Date('2024-01-15T10:30:00Z');
|
||||
const result = formatQuotedEmailDate(date);
|
||||
expect(result).toMatch(/Mon, Jan 15, 2024 at/);
|
||||
});
|
||||
|
||||
it('returns empty string for invalid date', () => {
|
||||
const result = formatQuotedEmailDate('invalid');
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInboxEmail', () => {
|
||||
it('returns email from contentAttributes.email.to', () => {
|
||||
const lastEmail = {
|
||||
contentAttributes: {
|
||||
email: { to: ['inbox@example.com'] },
|
||||
},
|
||||
};
|
||||
const result = getInboxEmail(lastEmail, {});
|
||||
expect(result).toBe('inbox@example.com');
|
||||
});
|
||||
|
||||
it('returns inbox email as fallback', () => {
|
||||
const lastEmail = {};
|
||||
const inbox = { email: 'support@example.com' };
|
||||
const result = getInboxEmail(lastEmail, inbox);
|
||||
expect(result).toBe('support@example.com');
|
||||
});
|
||||
|
||||
it('returns empty string if no email found', () => {
|
||||
expect(getInboxEmail({}, {})).toBe('');
|
||||
});
|
||||
|
||||
it('trims whitespace from emails', () => {
|
||||
const lastEmail = {
|
||||
contentAttributes: {
|
||||
email: { to: [' inbox@example.com '] },
|
||||
},
|
||||
};
|
||||
const result = getInboxEmail(lastEmail, {});
|
||||
expect(result).toBe('inbox@example.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildQuotedEmailHeaderFromContact', () => {
|
||||
it('builds complete header with name and email', () => {
|
||||
const lastEmail = {
|
||||
sender: { name: 'John Doe', email: 'john@example.com' },
|
||||
contentAttributes: {
|
||||
email: { date: '2024-01-15T10:30:00Z' },
|
||||
},
|
||||
};
|
||||
const result = buildQuotedEmailHeaderFromContact(lastEmail, {});
|
||||
expect(result).toContain('John Doe');
|
||||
expect(result).toContain('john@example.com');
|
||||
expect(result).toContain('wrote:');
|
||||
});
|
||||
|
||||
it('builds header without name if not available', () => {
|
||||
const lastEmail = {
|
||||
sender: { email: 'john@example.com' },
|
||||
contentAttributes: {
|
||||
email: { date: '2024-01-15T10:30:00Z' },
|
||||
},
|
||||
};
|
||||
const result = buildQuotedEmailHeaderFromContact(lastEmail, {});
|
||||
expect(result).toContain('<john@example.com>');
|
||||
expect(result).not.toContain('undefined');
|
||||
});
|
||||
|
||||
it('returns empty string if missing required data', () => {
|
||||
expect(buildQuotedEmailHeaderFromContact(null, {})).toBe('');
|
||||
expect(buildQuotedEmailHeaderFromContact({}, {})).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildQuotedEmailHeaderFromInbox', () => {
|
||||
it('builds complete header with inbox name and email', () => {
|
||||
const lastEmail = {
|
||||
contentAttributes: {
|
||||
email: {
|
||||
date: '2024-01-15T10:30:00Z',
|
||||
to: ['support@example.com'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const inbox = { name: 'Support Team', email: 'support@example.com' };
|
||||
const result = buildQuotedEmailHeaderFromInbox(lastEmail, inbox);
|
||||
expect(result).toContain('Support Team');
|
||||
expect(result).toContain('support@example.com');
|
||||
expect(result).toContain('wrote:');
|
||||
});
|
||||
|
||||
it('builds header without name if not available', () => {
|
||||
const lastEmail = {
|
||||
contentAttributes: {
|
||||
email: {
|
||||
date: '2024-01-15T10:30:00Z',
|
||||
to: ['inbox@example.com'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const inbox = { email: 'inbox@example.com' };
|
||||
const result = buildQuotedEmailHeaderFromInbox(lastEmail, inbox);
|
||||
expect(result).toContain('<inbox@example.com>');
|
||||
expect(result).not.toContain('undefined');
|
||||
});
|
||||
|
||||
it('returns empty string if missing required data', () => {
|
||||
expect(buildQuotedEmailHeaderFromInbox(null, {})).toBe('');
|
||||
expect(buildQuotedEmailHeaderFromInbox({}, {})).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildQuotedEmailHeader', () => {
|
||||
it('uses inbox email for outgoing messages (message_type: 1)', () => {
|
||||
const lastEmail = {
|
||||
message_type: 1,
|
||||
contentAttributes: {
|
||||
email: {
|
||||
date: '2024-01-15T10:30:00Z',
|
||||
to: ['support@example.com'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const inbox = { name: 'Support', email: 'support@example.com' };
|
||||
const contact = { name: 'John Doe', email: 'john@example.com' };
|
||||
const result = buildQuotedEmailHeader(lastEmail, contact, inbox);
|
||||
expect(result).toContain('Support');
|
||||
expect(result).toContain('support@example.com');
|
||||
expect(result).not.toContain('John Doe');
|
||||
});
|
||||
|
||||
it('uses contact email for incoming messages (message_type: 0)', () => {
|
||||
const lastEmail = {
|
||||
message_type: 0,
|
||||
sender: { name: 'Jane Smith', email: 'jane@example.com' },
|
||||
contentAttributes: {
|
||||
email: { date: '2024-01-15T10:30:00Z' },
|
||||
},
|
||||
};
|
||||
const inbox = { name: 'Support', email: 'support@example.com' };
|
||||
const contact = { name: 'Jane Smith', email: 'jane@example.com' };
|
||||
const result = buildQuotedEmailHeader(lastEmail, contact, inbox);
|
||||
expect(result).toContain('Jane Smith');
|
||||
expect(result).toContain('jane@example.com');
|
||||
expect(result).not.toContain('Support');
|
||||
});
|
||||
|
||||
it('returns empty string if missing required data', () => {
|
||||
expect(buildQuotedEmailHeader(null, {}, {})).toBe('');
|
||||
expect(buildQuotedEmailHeader({}, {}, {})).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatQuotedTextAsBlockquote', () => {
|
||||
it('formats single line text', () => {
|
||||
const result = formatQuotedTextAsBlockquote('Hello world');
|
||||
expect(result).toBe('> Hello world');
|
||||
});
|
||||
|
||||
it('formats multi-line text', () => {
|
||||
const text = 'Line 1\nLine 2\nLine 3';
|
||||
const result = formatQuotedTextAsBlockquote(text);
|
||||
expect(result).toBe('> Line 1\n> Line 2\n> Line 3');
|
||||
});
|
||||
|
||||
it('includes header if provided', () => {
|
||||
const result = formatQuotedTextAsBlockquote('Hello', 'Header text');
|
||||
expect(result).toContain('> Header text');
|
||||
expect(result).toContain('>\n> Hello');
|
||||
});
|
||||
|
||||
it('handles empty lines correctly', () => {
|
||||
const text = 'Line 1\n\nLine 3';
|
||||
const result = formatQuotedTextAsBlockquote(text);
|
||||
expect(result).toBe('> Line 1\n>\n> Line 3');
|
||||
});
|
||||
|
||||
it('returns empty string for empty input', () => {
|
||||
expect(formatQuotedTextAsBlockquote('')).toBe('');
|
||||
expect(formatQuotedTextAsBlockquote('', '')).toBe('');
|
||||
});
|
||||
|
||||
it('handles Windows line endings', () => {
|
||||
const text = 'Line 1\r\nLine 2';
|
||||
const result = formatQuotedTextAsBlockquote(text);
|
||||
expect(result).toBe('> Line 1\n> Line 2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractQuotedEmailText', () => {
|
||||
it('extracts text from textContent.reply', () => {
|
||||
const lastEmail = {
|
||||
contentAttributes: {
|
||||
email: { textContent: { reply: 'Reply text' } },
|
||||
},
|
||||
};
|
||||
const result = extractQuotedEmailText(lastEmail);
|
||||
expect(result).toBe('Reply text');
|
||||
});
|
||||
|
||||
it('falls back to textContent.full', () => {
|
||||
const lastEmail = {
|
||||
contentAttributes: {
|
||||
email: { textContent: { full: 'Full text' } },
|
||||
},
|
||||
};
|
||||
const result = extractQuotedEmailText(lastEmail);
|
||||
expect(result).toBe('Full text');
|
||||
});
|
||||
|
||||
it('extracts from htmlContent and converts to plain text', () => {
|
||||
const lastEmail = {
|
||||
contentAttributes: {
|
||||
email: { htmlContent: { reply: '<p>HTML reply</p>' } },
|
||||
},
|
||||
};
|
||||
const result = extractQuotedEmailText(lastEmail);
|
||||
expect(result).toBe('HTML reply');
|
||||
});
|
||||
|
||||
it('uses fallback content if structured content not available', () => {
|
||||
const lastEmail = { content: 'Fallback content' };
|
||||
const result = extractQuotedEmailText(lastEmail);
|
||||
expect(result).toBe('Fallback content');
|
||||
});
|
||||
|
||||
it('returns empty string for null or missing email', () => {
|
||||
expect(extractQuotedEmailText(null)).toBe('');
|
||||
expect(extractQuotedEmailText({})).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('truncatePreviewText', () => {
|
||||
it('returns full text if under max length', () => {
|
||||
const text = 'Short text';
|
||||
const result = truncatePreviewText(text, 80);
|
||||
expect(result).toBe('Short text');
|
||||
});
|
||||
|
||||
it('truncates text exceeding max length', () => {
|
||||
const text = 'A'.repeat(100);
|
||||
const result = truncatePreviewText(text, 80);
|
||||
expect(result).toHaveLength(80);
|
||||
expect(result).toContain('...');
|
||||
});
|
||||
|
||||
it('collapses multiple spaces', () => {
|
||||
const text = 'Text with spaces';
|
||||
const result = truncatePreviewText(text);
|
||||
expect(result).toBe('Text with spaces');
|
||||
});
|
||||
|
||||
it('trims whitespace', () => {
|
||||
const text = ' Text with spaces ';
|
||||
const result = truncatePreviewText(text);
|
||||
expect(result).toBe('Text with spaces');
|
||||
});
|
||||
|
||||
it('returns empty string for empty input', () => {
|
||||
expect(truncatePreviewText('')).toBe('');
|
||||
expect(truncatePreviewText(' ')).toBe('');
|
||||
});
|
||||
|
||||
it('uses default max length of 80', () => {
|
||||
const text = 'A'.repeat(100);
|
||||
const result = truncatePreviewText(text);
|
||||
expect(result).toHaveLength(80);
|
||||
});
|
||||
});
|
||||
|
||||
describe('appendQuotedTextToMessage', () => {
|
||||
it('appends quoted text to message', () => {
|
||||
const message = 'My reply';
|
||||
const quotedText = 'Original message';
|
||||
const header = 'On date sender wrote:';
|
||||
const result = appendQuotedTextToMessage(message, quotedText, header);
|
||||
|
||||
expect(result).toContain('My reply');
|
||||
expect(result).toContain('> On date sender wrote:');
|
||||
expect(result).toContain('> Original message');
|
||||
});
|
||||
|
||||
it('returns only quoted text if message is empty', () => {
|
||||
const result = appendQuotedTextToMessage('', 'Quoted', 'Header');
|
||||
expect(result).toContain('> Header');
|
||||
expect(result).toContain('> Quoted');
|
||||
expect(result).not.toContain('\n\n\n');
|
||||
});
|
||||
|
||||
it('returns message if no quoted text', () => {
|
||||
const result = appendQuotedTextToMessage('Message', '', '');
|
||||
expect(result).toBe('Message');
|
||||
});
|
||||
|
||||
it('handles proper spacing with double newline', () => {
|
||||
const result = appendQuotedTextToMessage('Message', 'Quoted', 'Header');
|
||||
expect(result).toContain('Message\n\n>');
|
||||
});
|
||||
|
||||
it('does not add extra newlines if message already ends with newlines', () => {
|
||||
const result = appendQuotedTextToMessage(
|
||||
'Message\n\n',
|
||||
'Quoted',
|
||||
'Header'
|
||||
);
|
||||
expect(result).not.toContain('\n\n\n');
|
||||
});
|
||||
|
||||
it('adds single newline if message ends with one newline', () => {
|
||||
const result = appendQuotedTextToMessage('Message\n', 'Quoted', 'Header');
|
||||
expect(result).toContain('Message\n\n>');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,241 @@
|
||||
import {
|
||||
getConversationDashboardRoute,
|
||||
isAConversationRoute,
|
||||
defaultRedirectPage,
|
||||
routeIsAccessibleFor,
|
||||
validateLoggedInRoutes,
|
||||
isAInboxViewRoute,
|
||||
} from '../routeHelpers';
|
||||
|
||||
describe('#routeIsAccessibleFor', () => {
|
||||
it('should return the correct access', () => {
|
||||
let route = { meta: { permissions: ['administrator'] } };
|
||||
expect(routeIsAccessibleFor(route, ['agent'])).toEqual(false);
|
||||
expect(routeIsAccessibleFor(route, ['administrator'])).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#defaultRedirectPage', () => {
|
||||
const to = {
|
||||
params: { accountId: '2' },
|
||||
fullPath: '/app/accounts/2/dashboard',
|
||||
name: 'home',
|
||||
};
|
||||
|
||||
it('should return dashboard route for users with conversation permissions', () => {
|
||||
const permissions = ['conversation_manage', 'agent'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
|
||||
});
|
||||
|
||||
it('should return contacts route for users with contact permissions', () => {
|
||||
const permissions = ['contact_manage'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/contacts');
|
||||
});
|
||||
|
||||
it('should return reports route for users with report permissions', () => {
|
||||
const permissions = ['report_manage'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe(
|
||||
'accounts/2/reports/overview'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return portals route for users with portal permissions', () => {
|
||||
const permissions = ['knowledge_base_manage'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/portals');
|
||||
});
|
||||
|
||||
it('should return dashboard route as default for users with custom roles', () => {
|
||||
const permissions = ['custom_role'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
|
||||
});
|
||||
|
||||
it('should return dashboard route for users with administrator role', () => {
|
||||
const permissions = ['administrator'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
|
||||
});
|
||||
|
||||
it('should return dashboard route for users with multiple permissions', () => {
|
||||
const permissions = [
|
||||
'contact_manage',
|
||||
'custom_role',
|
||||
'conversation_manage',
|
||||
'agent',
|
||||
'administrator',
|
||||
];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#validateLoggedInRoutes', () => {
|
||||
describe('when account access is missing', () => {
|
||||
it('should return the login route', () => {
|
||||
expect(
|
||||
validateLoggedInRoutes({ params: { accountId: 1 } }, { accounts: [] })
|
||||
).toEqual(`app/login`);
|
||||
});
|
||||
});
|
||||
describe('when account access is available', () => {
|
||||
describe('when account is suspended', () => {
|
||||
it('return suspended route', () => {
|
||||
expect(
|
||||
validateLoggedInRoutes(
|
||||
{
|
||||
name: 'conversations',
|
||||
params: { accountId: 1 },
|
||||
meta: { permissions: ['agent'] },
|
||||
},
|
||||
{ accounts: [{ id: 1, role: 'agent', status: 'suspended' }] }
|
||||
)
|
||||
).toEqual(`accounts/1/suspended`);
|
||||
});
|
||||
});
|
||||
describe('when account is active', () => {
|
||||
describe('when route is accessible', () => {
|
||||
it('returns null (no action required)', () => {
|
||||
expect(
|
||||
validateLoggedInRoutes(
|
||||
{
|
||||
name: 'conversations',
|
||||
params: { accountId: 1 },
|
||||
meta: { permissions: ['agent'] },
|
||||
},
|
||||
{
|
||||
permissions: ['agent'],
|
||||
accounts: [
|
||||
{
|
||||
id: 1,
|
||||
role: 'agent',
|
||||
permissions: ['agent'],
|
||||
status: 'active',
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
).toEqual(null);
|
||||
});
|
||||
});
|
||||
describe('when route is not accessible', () => {
|
||||
it('returns dashboard url', () => {
|
||||
expect(
|
||||
validateLoggedInRoutes(
|
||||
{
|
||||
name: 'billing',
|
||||
params: { accountId: 1 },
|
||||
meta: { permissions: ['administrator'] },
|
||||
},
|
||||
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] }
|
||||
)
|
||||
).toEqual(`accounts/1/dashboard`);
|
||||
});
|
||||
});
|
||||
describe('when route is suspended route', () => {
|
||||
it('returns dashboard url', () => {
|
||||
expect(
|
||||
validateLoggedInRoutes(
|
||||
{ name: 'account_suspended', params: { accountId: 1 } },
|
||||
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] }
|
||||
)
|
||||
).toEqual(`accounts/1/dashboard`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAConversationRoute', () => {
|
||||
it('returns true if conversation route name is provided', () => {
|
||||
expect(isAConversationRoute('inbox_conversation')).toBe(true);
|
||||
expect(isAConversationRoute('conversation_through_inbox')).toBe(true);
|
||||
expect(isAConversationRoute('conversations_through_label')).toBe(true);
|
||||
expect(isAConversationRoute('conversations_through_team')).toBe(true);
|
||||
expect(isAConversationRoute('dashboard')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true if base conversation route name is provided and includeBase is true', () => {
|
||||
expect(isAConversationRoute('home', true)).toBe(true);
|
||||
expect(isAConversationRoute('conversation_mentions', true)).toBe(true);
|
||||
expect(isAConversationRoute('conversation_unattended', true)).toBe(true);
|
||||
expect(isAConversationRoute('inbox_dashboard', true)).toBe(true);
|
||||
expect(isAConversationRoute('label_conversations', true)).toBe(true);
|
||||
expect(isAConversationRoute('team_conversations', true)).toBe(true);
|
||||
expect(isAConversationRoute('folder_conversations', true)).toBe(true);
|
||||
expect(isAConversationRoute('conversation_participating', true)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if base conversation route name is provided and includeBase is false', () => {
|
||||
expect(isAConversationRoute('home', false)).toBe(false);
|
||||
expect(isAConversationRoute('conversation_mentions', false)).toBe(false);
|
||||
expect(isAConversationRoute('conversation_unattended', false)).toBe(false);
|
||||
expect(isAConversationRoute('inbox_dashboard', false)).toBe(false);
|
||||
expect(isAConversationRoute('label_conversations', false)).toBe(false);
|
||||
expect(isAConversationRoute('team_conversations', false)).toBe(false);
|
||||
expect(isAConversationRoute('folder_conversations', false)).toBe(false);
|
||||
expect(isAConversationRoute('conversation_participating', false)).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('returns true if base conversation route name is provided and includeBase and includeExtended is true', () => {
|
||||
expect(isAConversationRoute('home', true, true)).toBe(true);
|
||||
expect(isAConversationRoute('conversation_mentions', true, true)).toBe(
|
||||
true
|
||||
);
|
||||
expect(isAConversationRoute('conversation_unattended', true, true)).toBe(
|
||||
true
|
||||
);
|
||||
expect(isAConversationRoute('inbox_dashboard', true, true)).toBe(true);
|
||||
expect(isAConversationRoute('label_conversations', true, true)).toBe(true);
|
||||
expect(isAConversationRoute('team_conversations', true, true)).toBe(true);
|
||||
expect(isAConversationRoute('folder_conversations', true, true)).toBe(true);
|
||||
expect(isAConversationRoute('conversation_participating', true, true)).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('returns false if base conversation route name is not provided', () => {
|
||||
expect(isAConversationRoute('')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConversationDashboardRoute', () => {
|
||||
it('returns dashboard route for conversation', () => {
|
||||
expect(getConversationDashboardRoute('inbox_conversation')).toEqual('home');
|
||||
expect(
|
||||
getConversationDashboardRoute('conversation_through_mentions')
|
||||
).toEqual('conversation_mentions');
|
||||
expect(
|
||||
getConversationDashboardRoute('conversation_through_unattended')
|
||||
).toEqual('conversation_unattended');
|
||||
expect(
|
||||
getConversationDashboardRoute('conversations_through_label')
|
||||
).toEqual('label_conversations');
|
||||
expect(getConversationDashboardRoute('conversations_through_team')).toEqual(
|
||||
'team_conversations'
|
||||
);
|
||||
expect(
|
||||
getConversationDashboardRoute('conversations_through_folders')
|
||||
).toEqual('folder_conversations');
|
||||
expect(
|
||||
getConversationDashboardRoute('conversation_through_participating')
|
||||
).toEqual('conversation_participating');
|
||||
expect(getConversationDashboardRoute('conversation_through_inbox')).toEqual(
|
||||
'inbox_dashboard'
|
||||
);
|
||||
expect(getConversationDashboardRoute('non_existent_route')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAInboxViewRoute', () => {
|
||||
it('returns true if inbox view route name is provided', () => {
|
||||
expect(isAInboxViewRoute('inbox_view_conversation')).toBe(true);
|
||||
expect(isAInboxViewRoute('inbox_conversation')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true if base inbox view route name is provided and includeBase is true', () => {
|
||||
expect(isAInboxViewRoute('inbox_view', true)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if base inbox view route name is provided and includeBase is false', () => {
|
||||
expect(isAInboxViewRoute('inbox_view')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
import {
|
||||
findSnoozeTime,
|
||||
snoozedReopenTime,
|
||||
findStartOfNextWeek,
|
||||
findStartOfNextMonth,
|
||||
findNextDay,
|
||||
setHoursToNine,
|
||||
snoozedReopenTimeToTimestamp,
|
||||
shortenSnoozeTime,
|
||||
} from '../snoozeHelpers';
|
||||
|
||||
describe('#Snooze Helpers', () => {
|
||||
describe('findStartOfNextWeek', () => {
|
||||
it('should return first working day of next week if a date is passed', () => {
|
||||
const today = new Date('06/16/2023');
|
||||
const startOfNextWeek = new Date('06/19/2023');
|
||||
expect(findStartOfNextWeek(today)).toEqual(startOfNextWeek);
|
||||
});
|
||||
it('should return first working day of next week if a date is passed', () => {
|
||||
const today = new Date('06/03/2023');
|
||||
const startOfNextWeek = new Date('06/05/2023');
|
||||
expect(findStartOfNextWeek(today)).toEqual(startOfNextWeek);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findStartOfNextMonth', () => {
|
||||
it('should return first working day of next month if a valid date is passed', () => {
|
||||
const today = new Date('06/21/2023');
|
||||
const startOfNextMonth = new Date('07/03/2023');
|
||||
expect(findStartOfNextMonth(today)).toEqual(startOfNextMonth);
|
||||
});
|
||||
it('should return first working day of next month if a valid date is passed', () => {
|
||||
const today = new Date('02/28/2023');
|
||||
const startOfNextMonth = new Date('03/06/2023');
|
||||
expect(findStartOfNextMonth(today)).toEqual(startOfNextMonth);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setHoursToNine', () => {
|
||||
it('should return date with 9.00AM time', () => {
|
||||
const nextDay = new Date('06/17/2023');
|
||||
nextDay.setHours(9, 0, 0, 0);
|
||||
expect(setHoursToNine(nextDay)).toEqual(nextDay);
|
||||
});
|
||||
it('should return date with 9.00AM time if date with 10am is passes', () => {
|
||||
const nextDay = new Date('06/17/2023 10:00:00');
|
||||
nextDay.setHours(9, 0, 0, 0);
|
||||
expect(setHoursToNine(nextDay)).toEqual(nextDay);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findSnoozeTime', () => {
|
||||
it('should return nil if until_next_reply is passed', () => {
|
||||
expect(findSnoozeTime('until_next_reply')).toEqual(null);
|
||||
});
|
||||
|
||||
it('should return next hour time stamp if an_hour_from_now is passed', () => {
|
||||
const nextHour = new Date();
|
||||
nextHour.setHours(nextHour.getHours() + 1);
|
||||
expect(findSnoozeTime('an_hour_from_now')).toBeCloseTo(
|
||||
Math.floor(nextHour.getTime() / 1000)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return next day 9.00AM time stamp until_tomorrow is passed', () => {
|
||||
const today = new Date('06/16/2023');
|
||||
const nextDay = new Date('06/17/2023');
|
||||
nextDay.setHours(9, 0, 0, 0);
|
||||
expect(findSnoozeTime('until_tomorrow', today)).toBeCloseTo(
|
||||
nextDay.getTime() / 1000
|
||||
);
|
||||
});
|
||||
|
||||
it('should return next week monday 9.00AM time stamp if until_next_week is passed', () => {
|
||||
const today = new Date('06/16/2023');
|
||||
const startOfNextWeek = new Date('06/19/2023');
|
||||
startOfNextWeek.setHours(9, 0, 0, 0);
|
||||
expect(findSnoozeTime('until_next_week', today)).toBeCloseTo(
|
||||
startOfNextWeek.getTime() / 1000
|
||||
);
|
||||
});
|
||||
|
||||
it('should return next month 9.00AM time stamp if until_next_month is passed', () => {
|
||||
const today = new Date('06/21/2023');
|
||||
const startOfNextMonth = new Date('07/03/2023');
|
||||
startOfNextMonth.setHours(9, 0, 0, 0);
|
||||
expect(findSnoozeTime('until_next_month', today)).toBeCloseTo(
|
||||
startOfNextMonth.getTime() / 1000
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('snoozedReopenTime', () => {
|
||||
it('should return nil if snoozedUntil is nil', () => {
|
||||
expect(snoozedReopenTime(null)).toEqual(null);
|
||||
});
|
||||
|
||||
it('should return formatted date if snoozedUntil is not nil', () => {
|
||||
expect(snoozedReopenTime('2023-06-07T09:00:00.000Z')).toEqual(
|
||||
'7 Jun, 9.00am'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findNextDay', () => {
|
||||
it('should return next day', () => {
|
||||
const today = new Date('06/16/2023');
|
||||
const nextDay = new Date('06/17/2023');
|
||||
expect(findNextDay(today)).toEqual(nextDay);
|
||||
});
|
||||
});
|
||||
|
||||
describe('snoozedReopenTimeToTimestamp', () => {
|
||||
it('should return timestamp if snoozedUntil is not nil', () => {
|
||||
expect(snoozedReopenTimeToTimestamp('2023-06-07T09:00:00.000Z')).toEqual(
|
||||
1686128400
|
||||
);
|
||||
});
|
||||
it('should return nil if snoozedUntil is nil', () => {
|
||||
expect(snoozedReopenTimeToTimestamp(null)).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shortenSnoozeTime', () => {
|
||||
it('should return shortened time if snoozedUntil is not nil and day is passed', () => {
|
||||
expect(shortenSnoozeTime('1 day')).toEqual('1d');
|
||||
});
|
||||
|
||||
it('should return shortened time if snoozedUntil is not nil and month is passed', () => {
|
||||
expect(shortenSnoozeTime('1 month')).toEqual('1mo');
|
||||
});
|
||||
|
||||
it('should return shortened time if snoozedUntil is not nil and year is passed', () => {
|
||||
expect(shortenSnoozeTime('1 year')).toEqual('1y');
|
||||
});
|
||||
|
||||
it('should return shortened time if snoozedUntil is not nil and hour is passed', () => {
|
||||
expect(shortenSnoozeTime('1 hour')).toEqual('1h');
|
||||
});
|
||||
|
||||
it('should return shortened time if snoozedUntil is not nil and minutes is passed', () => {
|
||||
expect(shortenSnoozeTime('1 minutes')).toEqual('1m');
|
||||
});
|
||||
|
||||
it('should return shortened time if snoozedUntil is not nil and in is passed', () => {
|
||||
expect(shortenSnoozeTime('in 1 hour')).toEqual('1h');
|
||||
});
|
||||
|
||||
it('should return nil if snoozedUntil is nil', () => {
|
||||
expect(shortenSnoozeTime(null)).toEqual(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,369 @@
|
||||
import {
|
||||
replaceTemplateVariables,
|
||||
buildTemplateParameters,
|
||||
processVariable,
|
||||
allKeysRequired,
|
||||
} from '../templateHelper';
|
||||
import { templates } from '../../store/modules/specs/inboxes/templateFixtures';
|
||||
|
||||
describe('templateHelper', () => {
|
||||
const technicianTemplate = templates.find(t => t.name === 'technician_visit');
|
||||
|
||||
describe('processVariable', () => {
|
||||
it('should remove curly braces from variables', () => {
|
||||
expect(processVariable('{{name}}')).toBe('name');
|
||||
expect(processVariable('{{1}}')).toBe('1');
|
||||
expect(processVariable('{{customer_id}}')).toBe('customer_id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('allKeysRequired', () => {
|
||||
it('should return true when all keys have values', () => {
|
||||
const obj = { name: 'John', age: '30' };
|
||||
expect(allKeysRequired(obj)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when some keys are empty', () => {
|
||||
const obj = { name: 'John', age: '' };
|
||||
expect(allKeysRequired(obj)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for empty object', () => {
|
||||
expect(allKeysRequired({})).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceTemplateVariables', () => {
|
||||
const templateText =
|
||||
"Hi {{1}}, we're scheduling a technician visit to {{2}} on {{3}} between {{4}} and {{5}}. Please confirm if this time slot works for you.";
|
||||
|
||||
it('should replace all variables with provided values', () => {
|
||||
const processedParams = {
|
||||
body: {
|
||||
1: 'John',
|
||||
2: '123 Main St',
|
||||
3: '2025-01-15',
|
||||
4: '10:00 AM',
|
||||
5: '2:00 PM',
|
||||
},
|
||||
};
|
||||
|
||||
const result = replaceTemplateVariables(templateText, processedParams);
|
||||
expect(result).toBe(
|
||||
"Hi John, we're scheduling a technician visit to 123 Main St on 2025-01-15 between 10:00 AM and 2:00 PM. Please confirm if this time slot works for you."
|
||||
);
|
||||
});
|
||||
|
||||
it('should keep original variable format when no replacement value provided', () => {
|
||||
const processedParams = {
|
||||
body: {
|
||||
1: 'John',
|
||||
3: '2025-01-15',
|
||||
},
|
||||
};
|
||||
|
||||
const result = replaceTemplateVariables(templateText, processedParams);
|
||||
expect(result).toContain('John');
|
||||
expect(result).toContain('2025-01-15');
|
||||
expect(result).toContain('{{2}}');
|
||||
expect(result).toContain('{{4}}');
|
||||
expect(result).toContain('{{5}}');
|
||||
});
|
||||
|
||||
it('should handle empty processedParams', () => {
|
||||
const result = replaceTemplateVariables(templateText, {});
|
||||
expect(result).toBe(templateText);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildTemplateParameters', () => {
|
||||
it('should build parameters for template with body variables', () => {
|
||||
const result = buildTemplateParameters(technicianTemplate, false);
|
||||
|
||||
expect(result.body).toEqual({
|
||||
1: '',
|
||||
2: '',
|
||||
3: '',
|
||||
4: '',
|
||||
5: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should include header parameters when hasMediaHeader is true', () => {
|
||||
const imageTemplate = templates.find(
|
||||
t => t.name === 'order_confirmation'
|
||||
);
|
||||
const result = buildTemplateParameters(imageTemplate, true);
|
||||
|
||||
expect(result.header).toEqual({
|
||||
media_url: '',
|
||||
media_type: 'image',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not include header parameters when hasMediaHeader is false', () => {
|
||||
const result = buildTemplateParameters(technicianTemplate, false);
|
||||
expect(result.header).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle template with no body component', () => {
|
||||
const templateWithoutBody = {
|
||||
components: [{ type: 'HEADER', format: 'TEXT' }],
|
||||
};
|
||||
|
||||
const result = buildTemplateParameters(templateWithoutBody, false);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle template with no variables', () => {
|
||||
const templateWithoutVars = templates.find(
|
||||
t => t.name === 'no_variable_template'
|
||||
);
|
||||
const result = buildTemplateParameters(templateWithoutVars, false);
|
||||
|
||||
expect(result.body).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle URL buttons with variables for non-authentication templates', () => {
|
||||
const templateWithUrlButton = {
|
||||
category: 'MARKETING',
|
||||
components: [
|
||||
{
|
||||
type: 'BODY',
|
||||
text: 'Check out our website at {{site_url}}',
|
||||
},
|
||||
{
|
||||
type: 'BUTTONS',
|
||||
buttons: [
|
||||
{
|
||||
type: 'URL',
|
||||
url: 'https://example.com/{{campaign_id}}',
|
||||
text: 'Visit Site',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = buildTemplateParameters(templateWithUrlButton, false);
|
||||
expect(result.buttons).toEqual([
|
||||
{
|
||||
type: 'url',
|
||||
parameter: '',
|
||||
url: 'https://example.com/{{campaign_id}}',
|
||||
variables: ['campaign_id'],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle templates with no variables', () => {
|
||||
const emptyTemplate = templates.find(
|
||||
t => t.name === 'no_variable_template'
|
||||
);
|
||||
const result = buildTemplateParameters(emptyTemplate, false);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should build parameters for templates with multiple component types', () => {
|
||||
const complexTemplate = {
|
||||
components: [
|
||||
{ type: 'HEADER', format: 'IMAGE' },
|
||||
{ type: 'BODY', text: 'Hi {{1}}, your order {{2}} is ready!' },
|
||||
{ type: 'FOOTER', text: 'Thank you for your business' },
|
||||
{
|
||||
type: 'BUTTONS',
|
||||
buttons: [{ type: 'URL', url: 'https://example.com/{{3}}' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = buildTemplateParameters(complexTemplate, true);
|
||||
|
||||
expect(result.header).toEqual({
|
||||
media_url: '',
|
||||
media_type: 'image',
|
||||
});
|
||||
expect(result.body).toEqual({ 1: '', 2: '' });
|
||||
expect(result.buttons).toEqual([
|
||||
{
|
||||
type: 'url',
|
||||
parameter: '',
|
||||
url: 'https://example.com/{{3}}',
|
||||
variables: ['3'],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle copy code buttons correctly', () => {
|
||||
const copyCodeTemplate = templates.find(
|
||||
t => t.name === 'discount_coupon'
|
||||
);
|
||||
const result = buildTemplateParameters(copyCodeTemplate, false);
|
||||
|
||||
expect(result.body).toBeDefined();
|
||||
expect(result.buttons).toEqual([
|
||||
{
|
||||
type: 'copy_code',
|
||||
parameter: '',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle templates with document headers', () => {
|
||||
const documentTemplate = templates.find(
|
||||
t => t.name === 'purchase_receipt'
|
||||
);
|
||||
const result = buildTemplateParameters(documentTemplate, true);
|
||||
|
||||
expect(result.header).toEqual({
|
||||
media_url: '',
|
||||
media_type: 'document',
|
||||
media_name: '',
|
||||
});
|
||||
expect(result.body).toEqual({
|
||||
1: '',
|
||||
2: '',
|
||||
3: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle video header templates', () => {
|
||||
const videoTemplate = templates.find(t => t.name === 'training_video');
|
||||
const result = buildTemplateParameters(videoTemplate, true);
|
||||
|
||||
expect(result.header).toEqual({
|
||||
media_url: '',
|
||||
media_type: 'video',
|
||||
});
|
||||
expect(result.body).toEqual({
|
||||
name: '',
|
||||
date: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('enhanced format validation', () => {
|
||||
it('should validate enhanced format structure', () => {
|
||||
const processedParams = {
|
||||
body: { 1: 'John', 2: 'Order123' },
|
||||
header: {
|
||||
media_url: 'https://example.com/image.jpg',
|
||||
media_type: 'image',
|
||||
},
|
||||
buttons: [{ type: 'copy_code', parameter: 'SAVE20' }],
|
||||
};
|
||||
|
||||
// Test that structure is properly formed
|
||||
expect(processedParams.body).toBeDefined();
|
||||
expect(typeof processedParams.body).toBe('object');
|
||||
expect(processedParams.header).toBeDefined();
|
||||
expect(Array.isArray(processedParams.buttons)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle empty component sections', () => {
|
||||
const processedParams = {
|
||||
body: {},
|
||||
header: {},
|
||||
buttons: [],
|
||||
};
|
||||
|
||||
expect(allKeysRequired(processedParams.body)).toBe(true);
|
||||
expect(allKeysRequired(processedParams.header)).toBe(true);
|
||||
expect(processedParams.buttons.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should validate parameter completeness', () => {
|
||||
const incompleteParams = {
|
||||
body: { 1: 'John', 2: '' },
|
||||
};
|
||||
|
||||
expect(allKeysRequired(incompleteParams.body)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle edge cases in processVariable', () => {
|
||||
expect(processVariable('{{')).toBe('');
|
||||
expect(processVariable('}}')).toBe('');
|
||||
expect(processVariable('')).toBe('');
|
||||
expect(processVariable('{{nested{{variable}}}}')).toBe('nestedvariable');
|
||||
});
|
||||
|
||||
it('should handle special characters in template variables', () => {
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
const templateText =
|
||||
'Welcome {{user_name}}, your order #{{order_id}} costs ${{amount}}';
|
||||
/* eslint-enable no-template-curly-in-string */
|
||||
const processedParams = {
|
||||
body: {
|
||||
user_name: 'John & Jane',
|
||||
order_id: '12345',
|
||||
amount: '99.99',
|
||||
},
|
||||
};
|
||||
|
||||
const result = replaceTemplateVariables(templateText, processedParams);
|
||||
expect(result).toBe(
|
||||
'Welcome John & Jane, your order #12345 costs $99.99'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle templates with mixed parameter types', () => {
|
||||
const mixedTemplate = {
|
||||
components: [
|
||||
{ type: 'HEADER', format: 'VIDEO' },
|
||||
{ type: 'BODY', text: 'Order {{order_id}} status: {{status}}' },
|
||||
{ type: 'FOOTER', text: 'Thank you' },
|
||||
{
|
||||
type: 'BUTTONS',
|
||||
buttons: [
|
||||
{ type: 'URL', url: 'https://track.com/{{order_id}}' },
|
||||
{ type: 'COPY_CODE' },
|
||||
{ type: 'PHONE_NUMBER', phone_number: '+1234567890' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = buildTemplateParameters(mixedTemplate, true);
|
||||
|
||||
expect(result.header).toEqual({
|
||||
media_url: '',
|
||||
media_type: 'video',
|
||||
});
|
||||
expect(result.body).toEqual({
|
||||
order_id: '',
|
||||
status: '',
|
||||
});
|
||||
expect(result.buttons).toHaveLength(2); // URL and COPY_CODE (PHONE_NUMBER doesn't need parameters)
|
||||
expect(result.buttons[0].type).toBe('url');
|
||||
expect(result.buttons[1].type).toBe('copy_code');
|
||||
});
|
||||
|
||||
it('should handle templates with no processable components', () => {
|
||||
const emptyTemplate = {
|
||||
components: [
|
||||
{ type: 'HEADER', format: 'TEXT', text: 'Static Header' },
|
||||
{ type: 'BODY', text: 'Static body with no variables' },
|
||||
{ type: 'FOOTER', text: 'Static footer' },
|
||||
],
|
||||
};
|
||||
|
||||
const result = buildTemplateParameters(emptyTemplate, false);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should validate that replaceTemplateVariables preserves unreplaced variables', () => {
|
||||
const templateText = 'Hi {{name}}, order {{order_id}} is {{status}}';
|
||||
const partialParams = {
|
||||
body: {
|
||||
name: 'John',
|
||||
// order_id missing
|
||||
status: 'ready',
|
||||
},
|
||||
};
|
||||
|
||||
const result = replaceTemplateVariables(templateText, partialParams);
|
||||
expect(result).toBe('Hi John, order {{order_id}} is ready');
|
||||
expect(result).toContain('{{order_id}}'); // Unreplaced variable preserved
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { setColorTheme } from 'dashboard/helper/themeHelper.js';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
|
||||
vi.mock('shared/helpers/localStorage');
|
||||
|
||||
describe('setColorTheme', () => {
|
||||
it('should set body class to dark if selectedColorScheme is dark', () => {
|
||||
LocalStorage.get.mockReturnValue('dark');
|
||||
setColorTheme(true);
|
||||
expect(document.body.classList.contains('dark')).toBe(true);
|
||||
});
|
||||
|
||||
it('should set body class to dark if selectedColorScheme is auto and isOSOnDarkMode is true', () => {
|
||||
LocalStorage.get.mockReturnValue('auto');
|
||||
setColorTheme(true);
|
||||
expect(document.body.classList.contains('dark')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not set body class to dark if selectedColorScheme is auto and isOSOnDarkMode is false', () => {
|
||||
LocalStorage.get.mockReturnValue('auto');
|
||||
setColorTheme(false);
|
||||
expect(document.body.classList.contains('dark')).toBe(false);
|
||||
});
|
||||
|
||||
it('should not set body class to dark if selectedColorScheme is light', () => {
|
||||
LocalStorage.get.mockReturnValue('light');
|
||||
setColorTheme(true);
|
||||
expect(document.body.classList.contains('dark')).toBe(false);
|
||||
});
|
||||
|
||||
it('should not set body class to dark if selectedColorScheme is undefined', () => {
|
||||
LocalStorage.get.mockReturnValue(undefined);
|
||||
setColorTheme(true);
|
||||
expect(document.body.classList.contains('dark')).toBe(true);
|
||||
});
|
||||
|
||||
it('should set documentElement style to dark if selectedColorScheme is dark', () => {
|
||||
LocalStorage.get.mockReturnValue('dark');
|
||||
setColorTheme(true);
|
||||
expect(document.documentElement.getAttribute('style')).toBe(
|
||||
'color-scheme: dark;'
|
||||
);
|
||||
});
|
||||
|
||||
it('should set documentElement style to dark if selectedColorScheme is auto and isOSOnDarkMode is true', () => {
|
||||
LocalStorage.get.mockReturnValue('auto');
|
||||
setColorTheme(true);
|
||||
expect(document.documentElement.getAttribute('style')).toBe(
|
||||
'color-scheme: dark;'
|
||||
);
|
||||
});
|
||||
|
||||
it('should set documentElement style to light if selectedColorScheme is auto and isOSOnDarkMode is false', () => {
|
||||
LocalStorage.get.mockReturnValue('auto');
|
||||
setColorTheme(false);
|
||||
expect(document.documentElement.getAttribute('style')).toBe(
|
||||
'color-scheme: light;'
|
||||
);
|
||||
});
|
||||
|
||||
it('should set documentElement style to light if selectedColorScheme is light', () => {
|
||||
LocalStorage.get.mockReturnValue('light');
|
||||
setColorTheme(true);
|
||||
expect(document.documentElement.getAttribute('style')).toBe(
|
||||
'color-scheme: light;'
|
||||
);
|
||||
});
|
||||
|
||||
it('should set documentElement style to light if selectedColorScheme is undefined', () => {
|
||||
LocalStorage.get.mockReturnValue(undefined);
|
||||
setColorTheme(true);
|
||||
expect(document.documentElement.getAttribute('style')).toBe(
|
||||
'color-scheme: dark;'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import axios from 'axios';
|
||||
import { uploadExternalImage, uploadFile } from '../uploadHelper';
|
||||
|
||||
global.axios = axios;
|
||||
vi.mock('axios');
|
||||
|
||||
describe('Upload Helpers', () => {
|
||||
afterEach(() => {
|
||||
axios.post.mockReset();
|
||||
});
|
||||
|
||||
describe('uploadFile', () => {
|
||||
it('should send a POST request with correct data', async () => {
|
||||
const mockFile = new File(['dummy content'], 'example.png', {
|
||||
type: 'image/png',
|
||||
});
|
||||
const mockResponse = {
|
||||
data: {
|
||||
file_url: 'https://example.com/fileUrl',
|
||||
blob_key: 'blobKey123',
|
||||
blob_id: 'blobId456',
|
||||
},
|
||||
};
|
||||
|
||||
axios.post.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await uploadFile(mockFile, '1602');
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'/api/v1/accounts/1602/upload',
|
||||
expect.any(FormData),
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
fileUrl: 'https://example.com/fileUrl',
|
||||
blobKey: 'blobKey123',
|
||||
blobId: 'blobId456',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle errors', async () => {
|
||||
const mockFile = new File(['dummy content'], 'example.png', {
|
||||
type: 'image/png',
|
||||
});
|
||||
const mockError = new Error('Failed to upload');
|
||||
|
||||
axios.post.mockRejectedValueOnce(mockError);
|
||||
|
||||
await expect(uploadFile(mockFile)).rejects.toThrow('Failed to upload');
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadExternalImage', () => {
|
||||
it('should send a POST request with correct data', async () => {
|
||||
const mockUrl = 'https://example.com/image.jpg';
|
||||
const mockResponse = {
|
||||
data: {
|
||||
file_url: 'https://example.com/fileUrl',
|
||||
blob_key: 'blobKey123',
|
||||
blob_id: 'blobId456',
|
||||
},
|
||||
};
|
||||
|
||||
axios.post.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await uploadExternalImage(mockUrl, '1602');
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'/api/v1/accounts/1602/upload',
|
||||
{ external_url: mockUrl },
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
fileUrl: 'https://example.com/fileUrl',
|
||||
blobKey: 'blobKey123',
|
||||
blobId: 'blobId456',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle errors', async () => {
|
||||
const mockUrl = 'https://example.com/image.jpg';
|
||||
const mockError = new Error('Failed to upload');
|
||||
|
||||
axios.post.mockRejectedValueOnce(mockError);
|
||||
|
||||
await expect(uploadExternalImage(mockUrl)).rejects.toThrow(
|
||||
'Failed to upload'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { validateAutomation } from '../validations';
|
||||
|
||||
describe('validateAutomation', () => {
|
||||
it('should return no errors for a valid automation', () => {
|
||||
const validAutomation = {
|
||||
name: 'Test Automation',
|
||||
description: 'A test automation',
|
||||
event_name: 'message_created',
|
||||
conditions: [
|
||||
{
|
||||
attribute_key: 'content',
|
||||
filter_operator: 'contains',
|
||||
values: 'hello',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{ action_name: 'send_message', action_params: ['Hello there!'] },
|
||||
],
|
||||
};
|
||||
const errors = validateAutomation(validAutomation);
|
||||
expect(errors).toEqual({});
|
||||
});
|
||||
|
||||
it('should return errors for missing basic fields', () => {
|
||||
const invalidAutomation = {
|
||||
name: '',
|
||||
description: '',
|
||||
event_name: '',
|
||||
conditions: [],
|
||||
actions: [],
|
||||
};
|
||||
const errors = validateAutomation(invalidAutomation);
|
||||
expect(errors).toHaveProperty('name');
|
||||
expect(errors).toHaveProperty('description');
|
||||
expect(errors).toHaveProperty('event_name');
|
||||
});
|
||||
|
||||
it('should return errors for invalid conditions', () => {
|
||||
const automationWithInvalidConditions = {
|
||||
name: 'Test',
|
||||
description: 'Test',
|
||||
event_name: 'message_created',
|
||||
conditions: [{ attribute_key: '', filter_operator: '', values: '' }],
|
||||
actions: [{ action_name: 'send_message', action_params: ['Hello'] }],
|
||||
};
|
||||
const errors = validateAutomation(automationWithInvalidConditions);
|
||||
expect(errors).toHaveProperty('condition_0');
|
||||
});
|
||||
|
||||
it('should return errors for invalid actions', () => {
|
||||
const automationWithInvalidActions = {
|
||||
name: 'Test',
|
||||
description: 'Test',
|
||||
event_name: 'message_created',
|
||||
conditions: [
|
||||
{
|
||||
attribute_key: 'content',
|
||||
filter_operator: 'contains',
|
||||
values: 'hello',
|
||||
},
|
||||
],
|
||||
actions: [{ action_name: 'send_message', action_params: [] }],
|
||||
};
|
||||
const errors = validateAutomation(automationWithInvalidActions);
|
||||
expect(errors).toHaveProperty('action_0');
|
||||
});
|
||||
|
||||
it('should not require action params for specific actions', () => {
|
||||
const automationWithNoParamAction = {
|
||||
name: 'Test',
|
||||
description: 'Test',
|
||||
event_name: 'message_created',
|
||||
conditions: [
|
||||
{
|
||||
attribute_key: 'content',
|
||||
filter_operator: 'contains',
|
||||
values: 'hello',
|
||||
},
|
||||
],
|
||||
actions: [{ action_name: 'mute_conversation' }],
|
||||
};
|
||||
const errors = validateAutomation(automationWithNoParamAction);
|
||||
expect(errors).toEqual({});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user