Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import { DuplicateContactException } from '../CustomErrors';
|
||||
|
||||
describe('DuplicateContactException', () => {
|
||||
it('returns correct exception', () => {
|
||||
const exception = new DuplicateContactException({
|
||||
attributes: ['email'],
|
||||
});
|
||||
expect(exception.message).toEqual('DUPLICATE_CONTACT');
|
||||
expect(exception.data).toEqual({
|
||||
attributes: ['email'],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { dispatchWindowEvent } from '../CustomEventHelper';
|
||||
|
||||
describe('dispatchWindowEvent', () => {
|
||||
it('dispatches correct event', () => {
|
||||
window.dispatchEvent = vi.fn();
|
||||
dispatchWindowEvent({ eventName: 'chatwoot:ready' });
|
||||
expect(dispatchEvent).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
formatDate,
|
||||
formatUnixDate,
|
||||
isTimeAfter,
|
||||
generateRelativeTime,
|
||||
} from '../DateHelper';
|
||||
|
||||
describe('#DateHelper', () => {
|
||||
it('should format unix date correctly without dateFormat', () => {
|
||||
expect(formatUnixDate(1576340626)).toEqual('Dec 14, 2019');
|
||||
});
|
||||
|
||||
it('should format unix date correctly without dateFormat', () => {
|
||||
expect(formatUnixDate(1608214031, 'MM/dd/yyyy')).toEqual('12/17/2020');
|
||||
});
|
||||
|
||||
it('should format date', () => {
|
||||
expect(
|
||||
formatDate({
|
||||
date: 'Dec 14, 2019',
|
||||
todayText: 'Today',
|
||||
yesterdayText: 'Yesterday',
|
||||
})
|
||||
).toEqual('Dec 14, 2019');
|
||||
});
|
||||
it('should format date as today ', () => {
|
||||
expect(
|
||||
formatDate({
|
||||
date: new Date(),
|
||||
todayText: 'Today',
|
||||
yesterdayText: 'Yesterday',
|
||||
})
|
||||
).toEqual('Today');
|
||||
});
|
||||
it('should format date as yesterday ', () => {
|
||||
const today = new Date();
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
expect(
|
||||
formatDate({
|
||||
date: yesterday,
|
||||
todayText: 'Today',
|
||||
yesterdayText: 'Yesterday',
|
||||
})
|
||||
).toEqual('Yesterday');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isTimeAfter', () => {
|
||||
it('return correct values', () => {
|
||||
expect(isTimeAfter(5, 30, 9, 30)).toEqual(false);
|
||||
expect(isTimeAfter(9, 30, 9, 30)).toEqual(true);
|
||||
expect(isTimeAfter(9, 29, 9, 30)).toEqual(false);
|
||||
expect(isTimeAfter(11, 59, 12, 0)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateRelativeTime', () => {
|
||||
it('should return a string with the relative time', () => {
|
||||
const value = 1;
|
||||
const unit = 'second';
|
||||
const languageCode = 'en-US';
|
||||
const expectedResult = 'in 1 second';
|
||||
|
||||
const actualResult = generateRelativeTime(value, unit, languageCode);
|
||||
|
||||
expect(actualResult).toBe(expectedResult);
|
||||
});
|
||||
|
||||
it('should return a string with the relative time in a different language', () => {
|
||||
const value = 10;
|
||||
const unit = 'minute';
|
||||
const languageCode = 'de-DE';
|
||||
const expectedResult = 'in 10 Minuten';
|
||||
|
||||
const actualResult = generateRelativeTime(value, unit, languageCode);
|
||||
|
||||
expect(actualResult).toBe(expectedResult);
|
||||
});
|
||||
|
||||
it('should return a string with the relative time for a different unit', () => {
|
||||
const value = 1;
|
||||
const unit = 'hour';
|
||||
const languageCode = 'en-US';
|
||||
const expectedResult = 'in 1 hour';
|
||||
|
||||
const actualResult = generateRelativeTime(value, unit, languageCode);
|
||||
|
||||
expect(actualResult).toBe(expectedResult);
|
||||
});
|
||||
|
||||
it('should throw an error if the value is not a number', () => {
|
||||
const value = 1;
|
||||
const unit = 'day';
|
||||
const languageCode = 'en_US';
|
||||
const expectedResult = 'tomorrow';
|
||||
|
||||
const actualResult = generateRelativeTime(value, unit, languageCode);
|
||||
|
||||
expect(actualResult).toBe(expectedResult);
|
||||
});
|
||||
|
||||
it('should throw an error if the value is not a number', () => {
|
||||
const value = 1;
|
||||
const unit = 'day';
|
||||
const languageCode = 'en-US';
|
||||
const expectedResult = 'tomorrow';
|
||||
|
||||
const actualResult = generateRelativeTime(value, unit, languageCode);
|
||||
|
||||
expect(actualResult).toBe(expectedResult);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { removeEmoji } from '../emoji';
|
||||
|
||||
describe('#removeEmoji', () => {
|
||||
it('returns values without emoji', () => {
|
||||
expect(removeEmoji('😄Hi👋🏻 there❕')).toEqual('Hi there');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,248 @@
|
||||
import {
|
||||
DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE,
|
||||
formatBytes,
|
||||
fileSizeInMegaBytes,
|
||||
checkFileSizeLimit,
|
||||
resolveMaximumFileUploadSize,
|
||||
isFileTypeAllowedForChannel,
|
||||
} from '../FileHelper';
|
||||
|
||||
describe('#File Helpers', () => {
|
||||
describe('formatBytes', () => {
|
||||
it('should return zero bytes if 0 is passed', () => {
|
||||
expect(formatBytes(0)).toBe('0 Bytes');
|
||||
});
|
||||
it('should return in bytes if 1000 is passed', () => {
|
||||
expect(formatBytes(1000)).toBe('1000 Bytes');
|
||||
});
|
||||
it('should return in KB if 100000 is passed', () => {
|
||||
expect(formatBytes(10000)).toBe('9.77 KB');
|
||||
});
|
||||
it('should return in MB if 10000000 is passed', () => {
|
||||
expect(formatBytes(10000000)).toBe('9.54 MB');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fileSizeInMegaBytes', () => {
|
||||
it('should return zero if 0 is passed', () => {
|
||||
expect(fileSizeInMegaBytes(0)).toBe(0);
|
||||
});
|
||||
it('should return 19.07 if 20000000 is passed', () => {
|
||||
expect(fileSizeInMegaBytes(20000000)).toBeCloseTo(19.07, 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkFileSizeLimit', () => {
|
||||
it('should return false if file with size 62208194 and file size limit 40 are passed', () => {
|
||||
expect(checkFileSizeLimit({ file: { size: 62208194 } }, 40)).toBe(false);
|
||||
});
|
||||
it('should return true if file with size 62208194 and file size limit 40 are passed', () => {
|
||||
expect(checkFileSizeLimit({ file: { size: 199154 } }, 40)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveMaximumFileUploadSize', () => {
|
||||
it('should return default when value is undefined', () => {
|
||||
expect(resolveMaximumFileUploadSize(undefined)).toBe(
|
||||
DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE
|
||||
);
|
||||
});
|
||||
|
||||
it('should return default when value is not a positive number', () => {
|
||||
expect(resolveMaximumFileUploadSize('not-a-number')).toBe(
|
||||
DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE
|
||||
);
|
||||
expect(resolveMaximumFileUploadSize(-5)).toBe(
|
||||
DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE
|
||||
);
|
||||
});
|
||||
|
||||
it('should parse numeric strings and numbers', () => {
|
||||
expect(resolveMaximumFileUploadSize('50')).toBe(50);
|
||||
expect(resolveMaximumFileUploadSize(75)).toBe(75);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFileTypeAllowedForChannel', () => {
|
||||
describe('edge cases', () => {
|
||||
it('should return false for null file', () => {
|
||||
expect(isFileTypeAllowedForChannel(null)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for undefined file', () => {
|
||||
expect(isFileTypeAllowedForChannel(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for file with zero size', () => {
|
||||
const file = { name: 'test.png', type: 'image/png', size: 0 };
|
||||
expect(isFileTypeAllowedForChannel(file)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('wildcard MIME types', () => {
|
||||
it('should allow image/png when image/* is allowed', () => {
|
||||
const file = { name: 'test.png', type: 'image/png', size: 1000 };
|
||||
expect(
|
||||
isFileTypeAllowedForChannel(file, {
|
||||
channelType: 'Channel::WebWidget',
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow image/jpeg when image/* is allowed', () => {
|
||||
const file = { name: 'test.jpg', type: 'image/jpeg', size: 1000 };
|
||||
expect(
|
||||
isFileTypeAllowedForChannel(file, {
|
||||
channelType: 'Channel::WebWidget',
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow audio/mp3 when audio/* is allowed', () => {
|
||||
const file = { name: 'test.mp3', type: 'audio/mp3', size: 1000 };
|
||||
expect(
|
||||
isFileTypeAllowedForChannel(file, {
|
||||
channelType: 'Channel::WebWidget',
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow video/mp4 when video/* is allowed', () => {
|
||||
const file = { name: 'test.mp4', type: 'video/mp4', size: 1000 };
|
||||
expect(
|
||||
isFileTypeAllowedForChannel(file, {
|
||||
channelType: 'Channel::WebWidget',
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exact MIME types', () => {
|
||||
it('should allow application/pdf when explicitly allowed', () => {
|
||||
const file = { name: 'test.pdf', type: 'application/pdf', size: 1000 };
|
||||
expect(
|
||||
isFileTypeAllowedForChannel(file, {
|
||||
channelType: 'Channel::WebWidget',
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow text/plain when explicitly allowed', () => {
|
||||
const file = { name: 'test.txt', type: 'text/plain', size: 1000 };
|
||||
expect(
|
||||
isFileTypeAllowedForChannel(file, {
|
||||
channelType: 'Channel::WebWidget',
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('file extensions', () => {
|
||||
it('should allow .3gpp extension when explicitly allowed', () => {
|
||||
const file = { name: 'test.3gpp', type: '', size: 1000 };
|
||||
expect(
|
||||
isFileTypeAllowedForChannel(file, {
|
||||
channelType: 'Channel::WebWidget',
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Instagram special handling', () => {
|
||||
it('should use Instagram rules when isInstagramChannel is true', () => {
|
||||
const file = { name: 'test.png', type: 'image/png', size: 1000 };
|
||||
expect(
|
||||
isFileTypeAllowedForChannel(file, {
|
||||
channelType: 'Channel::WebWidget',
|
||||
isInstagramChannel: true,
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should use Instagram rules when conversationType is instagram_direct_message', () => {
|
||||
const file = { name: 'test.png', type: 'image/png', size: 1000 };
|
||||
expect(
|
||||
isFileTypeAllowedForChannel(file, {
|
||||
channelType: 'Channel::WebWidget',
|
||||
conversationType: 'instagram_direct_message',
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('disallowed file types', () => {
|
||||
it('should reject executable files', () => {
|
||||
const file = {
|
||||
name: 'malware.exe',
|
||||
type: 'application/x-msdownload',
|
||||
size: 1000,
|
||||
};
|
||||
expect(
|
||||
isFileTypeAllowedForChannel(file, {
|
||||
channelType: 'Channel::WebWidget',
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject unsupported file types', () => {
|
||||
const file = {
|
||||
name: 'test.xyz',
|
||||
type: 'application/x-unknown',
|
||||
size: 1000,
|
||||
};
|
||||
expect(
|
||||
isFileTypeAllowedForChannel(file, {
|
||||
channelType: 'Channel::WebWidget',
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('channel-specific rules', () => {
|
||||
it('should allow WhatsApp-specific file types', () => {
|
||||
const file = { name: 'test.pdf', type: 'application/pdf', size: 1000 };
|
||||
expect(
|
||||
isFileTypeAllowedForChannel(file, {
|
||||
channelType: 'Channel::Whatsapp',
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow Twilio WhatsApp-specific file types', () => {
|
||||
const file = { name: 'test.pdf', type: 'application/pdf', size: 1000 };
|
||||
expect(
|
||||
isFileTypeAllowedForChannel(file, {
|
||||
channelType: 'Channel::TwilioSms',
|
||||
medium: 'whatsapp',
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('private note file types', () => {
|
||||
it('should allow broader file types for private notes', () => {
|
||||
const file = {
|
||||
name: 'test.pdf',
|
||||
type: 'application/pdf',
|
||||
size: 1000,
|
||||
};
|
||||
expect(
|
||||
isFileTypeAllowedForChannel(file, {
|
||||
channelType: 'Channel::Line',
|
||||
isOnPrivateNote: true,
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow CSV files in private notes', () => {
|
||||
const file = { name: 'data.csv', type: 'text/csv', size: 1000 };
|
||||
expect(
|
||||
isFileTypeAllowedForChannel(file, {
|
||||
channelType: 'Channel::Line',
|
||||
isOnPrivateNote: true,
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
isEnter,
|
||||
isEscape,
|
||||
hasPressedShift,
|
||||
hasPressedCommand,
|
||||
isActiveElementTypeable,
|
||||
} from '../KeyboardHelpers';
|
||||
|
||||
describe('#KeyboardHelpers', () => {
|
||||
describe('#isEnter', () => {
|
||||
it('return correct values', () => {
|
||||
expect(isEnter({ key: 'Enter' })).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isEscape', () => {
|
||||
it('return correct values', () => {
|
||||
expect(isEscape({ key: 'Escape' })).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#hasPressedShift', () => {
|
||||
it('return correct values', () => {
|
||||
expect(hasPressedShift({ shiftKey: true })).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#hasPressedCommand', () => {
|
||||
it('return correct values', () => {
|
||||
expect(hasPressedCommand({ metaKey: true })).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isActiveElementTypeable', () => {
|
||||
it('should return true if the active element is an input element', () => {
|
||||
const event = { target: document.createElement('input') };
|
||||
const result = isActiveElementTypeable(event);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if the active element is a textarea element', () => {
|
||||
const event = { target: document.createElement('textarea') };
|
||||
const result = isActiveElementTypeable(event);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if the active element is a contentEditable element', () => {
|
||||
const element = document.createElement('div');
|
||||
element.contentEditable = 'true';
|
||||
const event = { target: element };
|
||||
const result = isActiveElementTypeable(event);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the active element is not typeable', () => {
|
||||
const event = { target: document.createElement('div') };
|
||||
const result = isActiveElementTypeable(event);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if the active element is null', () => {
|
||||
const event = { target: null };
|
||||
const result = isActiveElementTypeable(event);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
import MessageFormatter from '../MessageFormatter';
|
||||
|
||||
describe('#MessageFormatter', () => {
|
||||
describe('content with links', () => {
|
||||
it('should format correctly', () => {
|
||||
const message =
|
||||
'Chatwoot is an opensource tool. [Chatwoot](https://www.chatwoot.com)';
|
||||
expect(new MessageFormatter(message).formattedMessage).toMatch(
|
||||
'<p>Chatwoot is an opensource tool. <a href="https://www.chatwoot.com" class="link" rel="noreferrer noopener nofollow" target="_blank">Chatwoot</a></p>'
|
||||
);
|
||||
});
|
||||
it('should format correctly', () => {
|
||||
const message =
|
||||
'Chatwoot is an opensource tool. https://www.chatwoot.com';
|
||||
expect(new MessageFormatter(message).formattedMessage).toMatch(
|
||||
'<p>Chatwoot is an opensource tool. <a href="https://www.chatwoot.com" class="link" rel="noreferrer noopener nofollow" target="_blank">https://www.chatwoot.com</a></p>'
|
||||
);
|
||||
});
|
||||
it('should not convert template variables to links when linkify is disabled', () => {
|
||||
const message = 'Hey {{customer.name}}, check https://chatwoot.com';
|
||||
const formatter = new MessageFormatter(message, false, false, false);
|
||||
expect(formatter.formattedMessage).toMatch(
|
||||
'<p>Hey {{customer.name}}, check https://chatwoot.com</p>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parses heading to strong', () => {
|
||||
it('should format correctly', () => {
|
||||
const message = '### opensource \n ## tool';
|
||||
expect(new MessageFormatter(message).formattedMessage).toMatch(
|
||||
`<h3>opensource</h3>
|
||||
<h2>tool</h2>`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('content with image and has "cw_image_height" query at the end of URL', () => {
|
||||
it('should set image height correctly', () => {
|
||||
const message =
|
||||
'Chatwoot is an opensource tool. ';
|
||||
expect(new MessageFormatter(message).formattedMessage).toMatch(
|
||||
'<p>Chatwoot is an opensource tool. <img src="http://chatwoot.com/chatwoot.png?cw_image_height=24px" alt="" style="height: 24px;" /></p>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should set image height correctly if its original size', () => {
|
||||
const message =
|
||||
'Chatwoot is an opensource tool. ';
|
||||
expect(new MessageFormatter(message).formattedMessage).toMatch(
|
||||
'<p>Chatwoot is an opensource tool. <img src="http://chatwoot.com/chatwoot.png?cw_image_height=auto" alt="" style="height: auto;" /></p>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not set height', () => {
|
||||
const message =
|
||||
'Chatwoot is an opensource tool. ';
|
||||
expect(new MessageFormatter(message).formattedMessage).toMatch(
|
||||
'<p>Chatwoot is an opensource tool. <img src="http://chatwoot.com/chatwoot.png" alt="" /></p>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tweets', () => {
|
||||
it('should return the same string if not tags or @mentions', () => {
|
||||
const message = 'Chatwoot is an opensource tool';
|
||||
expect(new MessageFormatter(message).formattedMessage).toMatch(message);
|
||||
});
|
||||
|
||||
it('should add links to @mentions', () => {
|
||||
const message =
|
||||
'@chatwootapp is an opensource tool thanks @longnonexistenttwitterusername';
|
||||
expect(
|
||||
new MessageFormatter(message, true, false).formattedMessage
|
||||
).toMatch(
|
||||
'<p><a href="http://twitter.com/chatwootapp" class="link" rel="noreferrer noopener nofollow" target="_blank">@chatwootapp</a> is an opensource tool thanks @longnonexistenttwitterusername</p>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should add links to #tags', () => {
|
||||
const message = '#chatwootapp is an opensource tool';
|
||||
expect(
|
||||
new MessageFormatter(message, true, false).formattedMessage
|
||||
).toMatch(
|
||||
'<p><a href="https://twitter.com/hashtag/chatwootapp" class="link" rel="noreferrer noopener nofollow" target="_blank">#chatwootapp</a> is an opensource tool</p>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('private notes', () => {
|
||||
it('should return the same string if not tags or @mentions', () => {
|
||||
const message = 'Chatwoot is an opensource tool';
|
||||
expect(new MessageFormatter(message).formattedMessage).toMatch(message);
|
||||
});
|
||||
|
||||
it('should add links to @mentions', () => {
|
||||
const message =
|
||||
'@chatwootapp is an opensource tool thanks @longnonexistenttwitterusername';
|
||||
expect(
|
||||
new MessageFormatter(message, false, true).formattedMessage
|
||||
).toMatch(message);
|
||||
});
|
||||
|
||||
it('should add links to #tags', () => {
|
||||
const message = '#chatwootapp is an opensource tool';
|
||||
expect(
|
||||
new MessageFormatter(message, false, true).formattedMessage
|
||||
).toMatch(message);
|
||||
});
|
||||
});
|
||||
|
||||
describe('plain text content', () => {
|
||||
it('returns the plain text without HTML', () => {
|
||||
const message =
|
||||
'<b>Chatwoot is an opensource tool. https://www.chatwoot.com</b>';
|
||||
expect(new MessageFormatter(message).plainText).toMatch(
|
||||
'Chatwoot is an opensource tool. https://www.chatwoot.com'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#sanitize', () => {
|
||||
it('sanitizes markup and removes all unnecessary elements', () => {
|
||||
const message =
|
||||
'[xssLink](javascript:alert(document.cookie))\n[normalLink](https://google.com)**I am a bold text paragraph**';
|
||||
expect(new MessageFormatter(message).formattedMessage).toMatch(
|
||||
`<p>[xssLink](javascript:alert(document.cookie))<br />
|
||||
<a href="https://google.com" class="link" rel="noreferrer noopener nofollow" target="_blank">normalLink</a><strong>I am a bold text paragraph</strong></p>`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { isASubmittedFormMessage, isAFormMessage } from '../MessageTypeHelper';
|
||||
|
||||
describe('#isASubmittedFormMessage', () => {
|
||||
it('should return correct value', () => {
|
||||
expect(
|
||||
isASubmittedFormMessage({
|
||||
content_type: 'form',
|
||||
content_attributes: {
|
||||
submitted_values: [{ name: 'text', value: 'Text ' }],
|
||||
},
|
||||
})
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isAFormMessage', () => {
|
||||
it('should return correct value', () => {
|
||||
expect(
|
||||
isAFormMessage({
|
||||
content_type: 'form',
|
||||
})
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,204 @@
|
||||
import {
|
||||
groupHeatmapByDay,
|
||||
reconcileHeatmapData,
|
||||
flattenHeatmapData,
|
||||
clampDataBetweenTimeline,
|
||||
} from '../ReportsDataHelper';
|
||||
|
||||
describe('flattenHeatmapData', () => {
|
||||
it('should flatten heatmap data to key-value pairs', () => {
|
||||
const data = [
|
||||
{ timestamp: 1614265200, value: 10 },
|
||||
{ timestamp: 1614308400, value: 20 },
|
||||
];
|
||||
const expected = {
|
||||
1614265200: 10,
|
||||
1614308400: 20,
|
||||
};
|
||||
expect(flattenHeatmapData(data)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should handle empty data', () => {
|
||||
const data = [];
|
||||
const expected = {};
|
||||
expect(flattenHeatmapData(data)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should handle data with same timestamps', () => {
|
||||
const data = [
|
||||
{ timestamp: 1614265200, value: 10 },
|
||||
{ timestamp: 1614265200, value: 20 },
|
||||
];
|
||||
const expected = {
|
||||
1614265200: 20,
|
||||
};
|
||||
expect(flattenHeatmapData(data)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reconcileHeatmapData', () => {
|
||||
it('should reconcile heatmap data with new data', () => {
|
||||
const data = [
|
||||
{ timestamp: 1614265200, value: 10 },
|
||||
{ timestamp: 1614308400, value: 20 },
|
||||
];
|
||||
const heatmapData = [
|
||||
{ timestamp: 1614265200, value: 5 },
|
||||
{ timestamp: 1614308400, value: 15 },
|
||||
{ timestamp: 1614387600, value: 25 },
|
||||
];
|
||||
const expected = [
|
||||
{ timestamp: 1614265200, value: 10 },
|
||||
{ timestamp: 1614308400, value: 20 },
|
||||
{ timestamp: 1614387600, value: 25 },
|
||||
];
|
||||
expect(reconcileHeatmapData(data, heatmapData)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should reconcile heatmap data with new data and handle missing data', () => {
|
||||
const data = [{ timestamp: 1614308400, value: 20 }];
|
||||
const heatmapData = [
|
||||
{ timestamp: 1614265200, value: 5 },
|
||||
{ timestamp: 1614308400, value: 15 },
|
||||
{ timestamp: 1614387600, value: 25 },
|
||||
];
|
||||
const expected = [
|
||||
{ timestamp: 1614265200, value: 5 },
|
||||
{ timestamp: 1614308400, value: 20 },
|
||||
{ timestamp: 1614387600, value: 25 },
|
||||
];
|
||||
expect(reconcileHeatmapData(data, heatmapData)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should replace empty heatmap data with a new array', () => {
|
||||
const data = [{ timestamp: 1614308400, value: 20 }];
|
||||
const heatmapData = [];
|
||||
expect(reconcileHeatmapData(data, heatmapData).length).toEqual(7 * 24);
|
||||
});
|
||||
});
|
||||
|
||||
describe('groupHeatmapByDay', () => {
|
||||
it('should group heatmap data by day', () => {
|
||||
const heatmapData = [
|
||||
{ timestamp: 1614265200, value: 10 },
|
||||
{ timestamp: 1614308400, value: 20 },
|
||||
{ timestamp: 1614387600, value: 30 },
|
||||
{ timestamp: 1614430800, value: 40 },
|
||||
{ timestamp: 1614499200, value: 50 },
|
||||
];
|
||||
|
||||
expect(groupHeatmapByDay(heatmapData)).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
"2021-02-25T00:00:00.000Z" => [
|
||||
{
|
||||
"date": 2021-02-25T15:00:00.000Z,
|
||||
"hour": 15,
|
||||
"timestamp": 1614265200,
|
||||
"value": 10,
|
||||
},
|
||||
],
|
||||
"2021-02-26T00:00:00.000Z" => [
|
||||
{
|
||||
"date": 2021-02-26T03:00:00.000Z,
|
||||
"hour": 3,
|
||||
"timestamp": 1614308400,
|
||||
"value": 20,
|
||||
},
|
||||
],
|
||||
"2021-02-27T00:00:00.000Z" => [
|
||||
{
|
||||
"date": 2021-02-27T01:00:00.000Z,
|
||||
"hour": 1,
|
||||
"timestamp": 1614387600,
|
||||
"value": 30,
|
||||
},
|
||||
{
|
||||
"date": 2021-02-27T13:00:00.000Z,
|
||||
"hour": 13,
|
||||
"timestamp": 1614430800,
|
||||
"value": 40,
|
||||
},
|
||||
],
|
||||
"2021-02-28T00:00:00.000Z" => [
|
||||
{
|
||||
"date": 2021-02-28T08:00:00.000Z,
|
||||
"hour": 8,
|
||||
"timestamp": 1614499200,
|
||||
"value": 50,
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should group empty heatmap data by day', () => {
|
||||
const heatmapData = [];
|
||||
const expected = new Map();
|
||||
expect(groupHeatmapByDay(heatmapData)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should group heatmap data with same timestamp in the same day', () => {
|
||||
const heatmapData = [
|
||||
{ timestamp: 1614265200, value: 10 },
|
||||
{ timestamp: 1614265200, value: 20 },
|
||||
];
|
||||
|
||||
expect(groupHeatmapByDay(heatmapData)).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
"2021-02-25T00:00:00.000Z" => [
|
||||
{
|
||||
"date": 2021-02-25T15:00:00.000Z,
|
||||
"hour": 15,
|
||||
"timestamp": 1614265200,
|
||||
"value": 10,
|
||||
},
|
||||
{
|
||||
"date": 2021-02-25T15:00:00.000Z,
|
||||
"hour": 15,
|
||||
"timestamp": 1614265200,
|
||||
"value": 20,
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clampDataBetweenTimeline', () => {
|
||||
const data = [
|
||||
{ timestamp: 1646054400, value: 'A' },
|
||||
{ timestamp: 1646054500, value: 'B' },
|
||||
{ timestamp: 1646054600, value: 'C' },
|
||||
{ timestamp: 1646054700, value: 'D' },
|
||||
{ timestamp: 1646054800, value: 'E' },
|
||||
];
|
||||
|
||||
it('should return empty array if data is empty', () => {
|
||||
expect(clampDataBetweenTimeline([], 1646054500, 1646054700)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array if no data is within the timeline', () => {
|
||||
expect(clampDataBetweenTimeline(data, 1646054900, 1646055000)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return the data as is no time limits are provider', () => {
|
||||
expect(clampDataBetweenTimeline(data)).toEqual(data);
|
||||
});
|
||||
|
||||
it('should return all data if all data is within the timeline', () => {
|
||||
expect(clampDataBetweenTimeline(data, 1646054300, 1646054900)).toEqual(
|
||||
data
|
||||
);
|
||||
});
|
||||
|
||||
it('should return only data within the timeline', () => {
|
||||
expect(clampDataBetweenTimeline(data, 1646054500, 1646054700)).toEqual([
|
||||
{ timestamp: 1646054500, value: 'B' },
|
||||
{ timestamp: 1646054600, value: 'C' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return empty array if from and to are the same', () => {
|
||||
expect(clampDataBetweenTimeline(data, 1646054500, 1646054500)).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
import {
|
||||
shouldBeUrl,
|
||||
isPhoneNumberValidWithDialCode,
|
||||
isPhoneE164OrEmpty,
|
||||
isPhoneE164,
|
||||
startsWithPlus,
|
||||
isValidPassword,
|
||||
isPhoneNumberValid,
|
||||
isNumber,
|
||||
isDomain,
|
||||
getRegexp,
|
||||
isValidSlug,
|
||||
} from '../Validators';
|
||||
|
||||
describe('#shouldBeUrl', () => {
|
||||
it('should return correct url', () => {
|
||||
expect(shouldBeUrl('http')).toEqual(true);
|
||||
});
|
||||
it('should return wrong url', () => {
|
||||
expect(shouldBeUrl('')).toEqual(true);
|
||||
expect(shouldBeUrl('abc')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isPhoneE164', () => {
|
||||
it('should return correct phone number', () => {
|
||||
expect(isPhoneE164('+1234567890')).toEqual(true);
|
||||
});
|
||||
it('should return wrong phone number', () => {
|
||||
expect(isPhoneE164('1234567890')).toEqual(false);
|
||||
expect(isPhoneE164('12345678A9')).toEqual(false);
|
||||
expect(isPhoneE164('+12345678901234567890')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isPhoneE164OrEmpty', () => {
|
||||
it('should return correct phone number', () => {
|
||||
expect(isPhoneE164OrEmpty('+1234567890')).toEqual(true);
|
||||
expect(isPhoneE164OrEmpty('')).toEqual(true);
|
||||
});
|
||||
it('should return wrong phone number', () => {
|
||||
expect(isPhoneE164OrEmpty('1234567890')).toEqual(false);
|
||||
expect(isPhoneE164OrEmpty('12345678A9')).toEqual(false);
|
||||
expect(isPhoneE164OrEmpty('+12345678901234567890')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isPhoneNumberValid', () => {
|
||||
it('should return correct phone number', () => {
|
||||
expect(isPhoneNumberValid('1234567890', '+91')).toEqual(true);
|
||||
});
|
||||
it('should return wrong phone number', () => {
|
||||
expect(isPhoneNumberValid('12345A67890', '+1')).toEqual(false);
|
||||
expect(isPhoneNumberValid('12345A6789120', '+1')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isValidPassword', () => {
|
||||
it('should return correct password', () => {
|
||||
expect(isValidPassword('testPass4!')).toEqual(true);
|
||||
expect(isValidPassword('testPass4-')).toEqual(true);
|
||||
expect(isValidPassword('testPass4\\')).toEqual(true);
|
||||
expect(isValidPassword("testPass4'")).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return wrong password', () => {
|
||||
expect(isValidPassword('testpass4')).toEqual(false);
|
||||
expect(isValidPassword('testPass4')).toEqual(false);
|
||||
expect(isValidPassword('testpass4!')).toEqual(false);
|
||||
expect(isValidPassword('testPass!')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isNumber', () => {
|
||||
it('should return correct number', () => {
|
||||
expect(isNumber('123')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return wrong number', () => {
|
||||
expect(isNumber('123-')).toEqual(false);
|
||||
expect(isNumber('123./')).toEqual(false);
|
||||
expect(isNumber('string')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isDomain', () => {
|
||||
it('should return correct domain', () => {
|
||||
expect(isDomain('test.com')).toEqual(true);
|
||||
expect(isDomain('www.test.com')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return wrong domain', () => {
|
||||
expect(isDomain('test')).toEqual(false);
|
||||
expect(isDomain('test.')).toEqual(false);
|
||||
expect(isDomain('test.123')).toEqual(false);
|
||||
expect(isDomain('http://www.test.com')).toEqual(false);
|
||||
expect(isDomain('https://test.in')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isPhoneNumberValidWithDialCode', () => {
|
||||
it('should return correct phone number', () => {
|
||||
expect(isPhoneNumberValidWithDialCode('+123456789')).toEqual(true);
|
||||
expect(isPhoneNumberValidWithDialCode('+12345')).toEqual(true);
|
||||
});
|
||||
it('should return wrong phone number', () => {
|
||||
expect(isPhoneNumberValidWithDialCode('+123')).toEqual(false);
|
||||
expect(isPhoneNumberValidWithDialCode('+1234')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#startsWithPlus', () => {
|
||||
it('should return correct phone number', () => {
|
||||
expect(startsWithPlus('+123456789')).toEqual(true);
|
||||
});
|
||||
it('should return wrong phone number', () => {
|
||||
expect(startsWithPlus('123456789')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getRegexp', () => {
|
||||
it('should create a correct RegExp object', () => {
|
||||
const regexPattern = '/^[a-z]+$/i';
|
||||
const regex = getRegexp(regexPattern);
|
||||
|
||||
expect(regex).toBeInstanceOf(RegExp);
|
||||
expect(regex.toString()).toBe(regexPattern);
|
||||
|
||||
expect(regex.test('abc')).toBe(true);
|
||||
expect(regex.test('ABC')).toBe(true);
|
||||
expect(regex.test('123')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle regex with flags', () => {
|
||||
const regexPattern = '/hello/gi';
|
||||
const regex = getRegexp(regexPattern);
|
||||
|
||||
expect(regex).toBeInstanceOf(RegExp);
|
||||
expect(regex.toString()).toBe(regexPattern);
|
||||
|
||||
expect(regex.test('hello')).toBe(true);
|
||||
expect(regex.test('HELLO')).toBe(false);
|
||||
expect(regex.test('Hello World')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle regex with special characters', () => {
|
||||
const regexPattern = '/\\d{3}-\\d{2}-\\d{4}/';
|
||||
const regex = getRegexp(regexPattern);
|
||||
|
||||
expect(regex).toBeInstanceOf(RegExp);
|
||||
expect(regex.toString()).toBe(regexPattern);
|
||||
|
||||
expect(regex.test('123-45-6789')).toBe(true);
|
||||
expect(regex.test('12-34-5678')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isValidSlug', () => {
|
||||
it('should return true for valid slugs', () => {
|
||||
expect(isValidSlug('abc')).toEqual(true);
|
||||
expect(isValidSlug('abc-123')).toEqual(true);
|
||||
expect(isValidSlug('a-b-c')).toEqual(true);
|
||||
expect(isValidSlug('123')).toEqual(true);
|
||||
expect(isValidSlug('abc123-def')).toEqual(true);
|
||||
});
|
||||
it('should return false for invalid slugs', () => {
|
||||
expect(isValidSlug('abc_def')).toEqual(false);
|
||||
expect(isValidSlug('abc def')).toEqual(false);
|
||||
expect(isValidSlug('abc@def')).toEqual(false);
|
||||
expect(isValidSlug('abc.def')).toEqual(false);
|
||||
expect(isValidSlug('abc/def')).toEqual(false);
|
||||
expect(isValidSlug('abc!def')).toEqual(false);
|
||||
expect(isValidSlug('abc--def!')).toEqual(false);
|
||||
expect(isValidSlug('abc-def ')).toEqual(false);
|
||||
expect(isValidSlug(' abc-def')).toEqual(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
import { getFromCache, setCache, clearCache } from '../cache';
|
||||
import { LocalStorage } from '../localStorage';
|
||||
|
||||
vi.mock('../localStorage');
|
||||
|
||||
describe('Cache Helpers', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(2023, 1, 1, 0, 0, 0));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('getFromCache', () => {
|
||||
it('returns null when no data is cached', () => {
|
||||
LocalStorage.get.mockReturnValue(null);
|
||||
|
||||
const result = getFromCache('test-key');
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(LocalStorage.get).toHaveBeenCalledWith('test-key');
|
||||
});
|
||||
|
||||
it('returns cached data when not expired', () => {
|
||||
// Current time is 2023-02-01 00:00:00
|
||||
// Cache timestamp is 1 hour ago
|
||||
const oneHourAgo =
|
||||
new Date(2023, 1, 1, 0, 0, 0).getTime() - 60 * 60 * 1000;
|
||||
|
||||
LocalStorage.get.mockReturnValue({
|
||||
data: { foo: 'bar' },
|
||||
timestamp: oneHourAgo,
|
||||
});
|
||||
|
||||
// Default expiry is 24 hours
|
||||
const result = getFromCache('test-key');
|
||||
|
||||
expect(result).toEqual({ foo: 'bar' });
|
||||
expect(LocalStorage.get).toHaveBeenCalledWith('test-key');
|
||||
expect(LocalStorage.remove).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('removes and returns null when data is expired', () => {
|
||||
// Current time is 2023-02-01 00:00:00
|
||||
// Cache timestamp is 25 hours ago (beyond the default 24-hour expiry)
|
||||
const twentyFiveHoursAgo =
|
||||
new Date(2023, 1, 1, 0, 0, 0).getTime() - 25 * 60 * 60 * 1000;
|
||||
|
||||
LocalStorage.get.mockReturnValue({
|
||||
data: { foo: 'bar' },
|
||||
timestamp: twentyFiveHoursAgo,
|
||||
});
|
||||
|
||||
const result = getFromCache('test-key');
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(LocalStorage.get).toHaveBeenCalledWith('test-key');
|
||||
expect(LocalStorage.remove).toHaveBeenCalledWith('test-key');
|
||||
});
|
||||
|
||||
it('respects custom expiry time', () => {
|
||||
// Current time is 2023-02-01 00:00:00
|
||||
// Cache timestamp is 2 hours ago
|
||||
const twoHoursAgo =
|
||||
new Date(2023, 1, 1, 0, 0, 0).getTime() - 2 * 60 * 60 * 1000;
|
||||
|
||||
LocalStorage.get.mockReturnValue({
|
||||
data: { foo: 'bar' },
|
||||
timestamp: twoHoursAgo,
|
||||
});
|
||||
|
||||
// Set expiry to 1 hour
|
||||
const result = getFromCache('test-key', 60 * 60 * 1000);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(LocalStorage.get).toHaveBeenCalledWith('test-key');
|
||||
expect(LocalStorage.remove).toHaveBeenCalledWith('test-key');
|
||||
});
|
||||
|
||||
it('handles errors gracefully', () => {
|
||||
LocalStorage.get.mockImplementation(() => {
|
||||
throw new Error('Storage error');
|
||||
});
|
||||
|
||||
const result = getFromCache('test-key');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setCache', () => {
|
||||
it('stores data with timestamp', () => {
|
||||
const data = { name: 'test' };
|
||||
const expectedCacheData = {
|
||||
data,
|
||||
timestamp: new Date(2023, 1, 1, 0, 0, 0).getTime(),
|
||||
};
|
||||
|
||||
setCache('test-key', data);
|
||||
|
||||
expect(LocalStorage.set).toHaveBeenCalledWith(
|
||||
'test-key',
|
||||
expectedCacheData
|
||||
);
|
||||
});
|
||||
|
||||
it('handles errors gracefully', () => {
|
||||
LocalStorage.set.mockImplementation(() => {
|
||||
throw new Error('Storage error');
|
||||
});
|
||||
|
||||
// Should not throw
|
||||
expect(() => setCache('test-key', { foo: 'bar' })).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearCache', () => {
|
||||
it('removes cached data', () => {
|
||||
clearCache('test-key');
|
||||
|
||||
expect(LocalStorage.remove).toHaveBeenCalledWith('test-key');
|
||||
});
|
||||
|
||||
it('handles errors gracefully', () => {
|
||||
LocalStorage.remove.mockImplementation(() => {
|
||||
throw new Error('Storage error');
|
||||
});
|
||||
|
||||
// Should not throw
|
||||
expect(() => clearCache('test-key')).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,284 @@
|
||||
import { copyTextToClipboard, handleOtpPaste } from '../clipboard';
|
||||
|
||||
const mockWriteText = vi.fn();
|
||||
Object.assign(navigator, {
|
||||
clipboard: {
|
||||
writeText: mockWriteText,
|
||||
},
|
||||
});
|
||||
|
||||
describe('copyTextToClipboard', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('with string input', () => {
|
||||
it('copies plain text string to clipboard', async () => {
|
||||
const text = 'Hello World';
|
||||
await copyTextToClipboard(text);
|
||||
|
||||
expect(mockWriteText).toHaveBeenCalledWith('Hello World');
|
||||
expect(mockWriteText).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('copies empty string to clipboard', async () => {
|
||||
const text = '';
|
||||
await copyTextToClipboard(text);
|
||||
|
||||
expect(mockWriteText).toHaveBeenCalledWith('');
|
||||
expect(mockWriteText).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with number input', () => {
|
||||
it('converts number to string', async () => {
|
||||
await copyTextToClipboard(42);
|
||||
|
||||
expect(mockWriteText).toHaveBeenCalledWith('42');
|
||||
expect(mockWriteText).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('converts zero to string', async () => {
|
||||
await copyTextToClipboard(0);
|
||||
|
||||
expect(mockWriteText).toHaveBeenCalledWith('0');
|
||||
expect(mockWriteText).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with boolean input', () => {
|
||||
it('converts true to string', async () => {
|
||||
await copyTextToClipboard(true);
|
||||
|
||||
expect(mockWriteText).toHaveBeenCalledWith('true');
|
||||
expect(mockWriteText).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('converts false to string', async () => {
|
||||
await copyTextToClipboard(false);
|
||||
|
||||
expect(mockWriteText).toHaveBeenCalledWith('false');
|
||||
expect(mockWriteText).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with null/undefined input', () => {
|
||||
it('converts null to empty string', async () => {
|
||||
await copyTextToClipboard(null);
|
||||
|
||||
expect(mockWriteText).toHaveBeenCalledWith('');
|
||||
expect(mockWriteText).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('converts undefined to empty string', async () => {
|
||||
await copyTextToClipboard(undefined);
|
||||
|
||||
expect(mockWriteText).toHaveBeenCalledWith('');
|
||||
expect(mockWriteText).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with object input', () => {
|
||||
it('stringifies simple object with proper formatting', async () => {
|
||||
const obj = { name: 'John', age: 30 };
|
||||
await copyTextToClipboard(obj);
|
||||
|
||||
const expectedJson = JSON.stringify(obj, null, 2);
|
||||
expect(mockWriteText).toHaveBeenCalledWith(expectedJson);
|
||||
expect(mockWriteText).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('stringifies nested object with proper formatting', async () => {
|
||||
const nestedObj = {
|
||||
severity: {
|
||||
user_id: 1181505,
|
||||
user_name: 'test',
|
||||
server_name: '[1253]test1253',
|
||||
},
|
||||
};
|
||||
await copyTextToClipboard(nestedObj);
|
||||
|
||||
const expectedJson = JSON.stringify(nestedObj, null, 2);
|
||||
expect(mockWriteText).toHaveBeenCalledWith(expectedJson);
|
||||
expect(mockWriteText).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('stringifies array with proper formatting', async () => {
|
||||
const arr = [1, 2, { name: 'test' }];
|
||||
await copyTextToClipboard(arr);
|
||||
|
||||
const expectedJson = JSON.stringify(arr, null, 2);
|
||||
expect(mockWriteText).toHaveBeenCalledWith(expectedJson);
|
||||
expect(mockWriteText).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('stringifies empty object', async () => {
|
||||
const obj = {};
|
||||
await copyTextToClipboard(obj);
|
||||
|
||||
expect(mockWriteText).toHaveBeenCalledWith('{}');
|
||||
expect(mockWriteText).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('stringifies empty array', async () => {
|
||||
const arr = [];
|
||||
await copyTextToClipboard(arr);
|
||||
|
||||
expect(mockWriteText).toHaveBeenCalledWith('[]');
|
||||
expect(mockWriteText).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('throws error when clipboard API fails', async () => {
|
||||
const error = new Error('Clipboard access denied');
|
||||
mockWriteText.mockRejectedValueOnce(error);
|
||||
|
||||
await expect(copyTextToClipboard('test')).rejects.toThrow(
|
||||
'Unable to copy text to clipboard: Clipboard access denied'
|
||||
);
|
||||
});
|
||||
|
||||
it('handles clipboard API not available', async () => {
|
||||
// Temporarily remove clipboard API
|
||||
const originalClipboard = navigator.clipboard;
|
||||
delete navigator.clipboard;
|
||||
|
||||
await expect(copyTextToClipboard('test')).rejects.toThrow(
|
||||
'Unable to copy text to clipboard:'
|
||||
);
|
||||
|
||||
// Restore clipboard API
|
||||
navigator.clipboard = originalClipboard;
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('handles Date objects', async () => {
|
||||
const date = new Date('2023-01-01T00:00:00.000Z');
|
||||
await copyTextToClipboard(date);
|
||||
|
||||
const expectedJson = JSON.stringify(date, null, 2);
|
||||
expect(mockWriteText).toHaveBeenCalledWith(expectedJson);
|
||||
expect(mockWriteText).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('handles functions by converting to string', async () => {
|
||||
const func = () => 'test';
|
||||
await copyTextToClipboard(func);
|
||||
|
||||
expect(mockWriteText).toHaveBeenCalledWith(func.toString());
|
||||
expect(mockWriteText).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleOtpPaste', () => {
|
||||
// Helper function to create mock clipboard event
|
||||
const createMockPasteEvent = text => ({
|
||||
clipboardData: {
|
||||
getData: vi.fn().mockReturnValue(text),
|
||||
},
|
||||
});
|
||||
|
||||
describe('valid OTP paste scenarios', () => {
|
||||
it('extracts 6-digit OTP from clean numeric string', () => {
|
||||
const event = createMockPasteEvent('123456');
|
||||
const result = handleOtpPaste(event);
|
||||
|
||||
expect(result).toBe('123456');
|
||||
expect(event.clipboardData.getData).toHaveBeenCalledWith('text');
|
||||
});
|
||||
|
||||
it('extracts 6-digit OTP from string with spaces', () => {
|
||||
const event = createMockPasteEvent('1 2 3 4 5 6');
|
||||
const result = handleOtpPaste(event);
|
||||
|
||||
expect(result).toBe('123456');
|
||||
});
|
||||
|
||||
it('extracts 6-digit OTP from string with dashes', () => {
|
||||
const event = createMockPasteEvent('123-456');
|
||||
const result = handleOtpPaste(event);
|
||||
|
||||
expect(result).toBe('123456');
|
||||
});
|
||||
|
||||
it('handles negative numbers by extracting digits only', () => {
|
||||
const event = createMockPasteEvent('-123456');
|
||||
const result = handleOtpPaste(event);
|
||||
|
||||
expect(result).toBe('123456');
|
||||
});
|
||||
|
||||
it('handles decimal numbers by extracting digits only', () => {
|
||||
const event = createMockPasteEvent('123.456');
|
||||
const result = handleOtpPaste(event);
|
||||
|
||||
expect(result).toBe('123456');
|
||||
});
|
||||
|
||||
it('extracts 6-digit OTP from mixed alphanumeric string', () => {
|
||||
const event = createMockPasteEvent('Your code is: 987654');
|
||||
const result = handleOtpPaste(event);
|
||||
|
||||
expect(result).toBe('987654');
|
||||
});
|
||||
|
||||
it('extracts first 6 digits when more than 6 digits present', () => {
|
||||
const event = createMockPasteEvent('12345678901234');
|
||||
const result = handleOtpPaste(event);
|
||||
|
||||
expect(result).toBe('123456');
|
||||
});
|
||||
|
||||
it('handles custom maxLength parameter', () => {
|
||||
const event = createMockPasteEvent('12345678');
|
||||
const result = handleOtpPaste(event, 8);
|
||||
|
||||
expect(result).toBe('12345678');
|
||||
});
|
||||
|
||||
it('extracts 4-digit OTP with custom maxLength', () => {
|
||||
const event = createMockPasteEvent('Your PIN: 9876');
|
||||
const result = handleOtpPaste(event, 4);
|
||||
|
||||
expect(result).toBe('9876');
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid OTP paste scenarios', () => {
|
||||
it('returns null for insufficient digits', () => {
|
||||
const event = createMockPasteEvent('12345');
|
||||
const result = handleOtpPaste(event);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for text with no digits', () => {
|
||||
const event = createMockPasteEvent('Hello World');
|
||||
const result = handleOtpPaste(event);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for empty string', () => {
|
||||
const event = createMockPasteEvent('');
|
||||
const result = handleOtpPaste(event);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when event is null', () => {
|
||||
const result = handleOtpPaste(null);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when event is undefined', () => {
|
||||
const result = handleOtpPaste(undefined);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { toHex, getContrast } from 'color2k';
|
||||
import {
|
||||
isWidgetColorLighter,
|
||||
adjustColorForContrast,
|
||||
} from 'shared/helpers/colorHelper';
|
||||
|
||||
describe('#isWidgetColorLighter', () => {
|
||||
it('returns true if color is lighter', () => {
|
||||
expect(isWidgetColorLighter('#ffffff')).toEqual(true);
|
||||
});
|
||||
it('returns false if color is darker', () => {
|
||||
expect(isWidgetColorLighter('#000000')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#adjustColorForContrast', () => {
|
||||
const targetRatio = 3.1;
|
||||
|
||||
const getContrastRatio = (color1, color2) => {
|
||||
// getContrast from 'color2k'
|
||||
return getContrast(color1, color2);
|
||||
};
|
||||
|
||||
it('adjusts a color to meet the contrast ratio against a light background', () => {
|
||||
const color = '#ff0000';
|
||||
const backgroundColor = '#ffffff';
|
||||
const adjustedColor = adjustColorForContrast(color, backgroundColor);
|
||||
const ratio = getContrastRatio(adjustedColor, backgroundColor);
|
||||
|
||||
expect(ratio).toBeGreaterThanOrEqual(targetRatio);
|
||||
});
|
||||
|
||||
it('adjusts a color to meet the contrast ratio against a dark background', () => {
|
||||
const color = '#00ff00';
|
||||
const backgroundColor = '#000000';
|
||||
const adjustedColor = adjustColorForContrast(color, backgroundColor);
|
||||
const ratio = getContrastRatio(adjustedColor, backgroundColor);
|
||||
|
||||
expect(ratio).toBeGreaterThanOrEqual(targetRatio);
|
||||
});
|
||||
|
||||
it('returns a string representation of the color', () => {
|
||||
const color = '#00ff00';
|
||||
const backgroundColor = '#000000';
|
||||
const adjustedColor = adjustColorForContrast(color, backgroundColor);
|
||||
|
||||
expect(typeof adjustedColor).toEqual('string');
|
||||
});
|
||||
|
||||
it('handles cases where the color already meets the contrast ratio', () => {
|
||||
const color = '#000000';
|
||||
const backgroundColor = '#ffffff';
|
||||
const adjustedColor = adjustColorForContrast(color, backgroundColor);
|
||||
const ratio = getContrastRatio(adjustedColor, backgroundColor);
|
||||
|
||||
expect(ratio).toBeGreaterThanOrEqual(targetRatio);
|
||||
expect(adjustedColor).toEqual(toHex(color));
|
||||
});
|
||||
|
||||
it('does not modify a color that already exceeds the contrast ratio', () => {
|
||||
const color = '#000000';
|
||||
const backgroundColor = '#ffffff';
|
||||
const adjustedColor = adjustColorForContrast(color, backgroundColor);
|
||||
|
||||
expect(adjustedColor).toEqual(toHex(color));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
isPdfDocument,
|
||||
formatDocumentLink,
|
||||
} from 'shared/helpers/documentHelper';
|
||||
|
||||
describe('documentHelper', () => {
|
||||
describe('#isPdfDocument', () => {
|
||||
it('returns true for PDF documents', () => {
|
||||
expect(isPdfDocument('PDF:document.pdf')).toBe(true);
|
||||
expect(isPdfDocument('PDF:my-file_20241227123045.pdf')).toBe(true);
|
||||
expect(isPdfDocument('PDF:report with spaces_20241227123045.pdf')).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('returns false for regular URLs', () => {
|
||||
expect(isPdfDocument('https://example.com')).toBe(false);
|
||||
expect(isPdfDocument('http://docs.example.com/file.pdf')).toBe(false);
|
||||
expect(isPdfDocument('ftp://files.example.com/document.pdf')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for empty or null values', () => {
|
||||
expect(isPdfDocument('')).toBe(false);
|
||||
expect(isPdfDocument(null)).toBe(false);
|
||||
expect(isPdfDocument(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for strings that contain PDF but do not start with PDF:', () => {
|
||||
expect(isPdfDocument('document PDF:file.pdf')).toBe(false);
|
||||
expect(isPdfDocument('My PDF:file.pdf')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#formatDocumentLink', () => {
|
||||
describe('PDF documents', () => {
|
||||
it('removes PDF: prefix from PDF documents', () => {
|
||||
expect(formatDocumentLink('PDF:document.pdf')).toBe('document.pdf');
|
||||
expect(formatDocumentLink('PDF:my-file.pdf')).toBe('my-file.pdf');
|
||||
});
|
||||
|
||||
it('removes timestamp suffix from PDF documents', () => {
|
||||
expect(formatDocumentLink('PDF:document_20241227123045.pdf')).toBe(
|
||||
'document.pdf'
|
||||
);
|
||||
expect(formatDocumentLink('PDF:report_20231215094530.pdf')).toBe(
|
||||
'report.pdf'
|
||||
);
|
||||
});
|
||||
|
||||
it('handles PDF documents with spaces in filename', () => {
|
||||
expect(formatDocumentLink('PDF:my document_20241227123045.pdf')).toBe(
|
||||
'my document.pdf'
|
||||
);
|
||||
expect(
|
||||
formatDocumentLink('PDF:Annual Report 2024_20241227123045.pdf')
|
||||
).toBe('Annual Report 2024.pdf');
|
||||
});
|
||||
|
||||
it('handles PDF documents without timestamp suffix', () => {
|
||||
expect(formatDocumentLink('PDF:document.pdf')).toBe('document.pdf');
|
||||
expect(formatDocumentLink('PDF:simple-file.pdf')).toBe(
|
||||
'simple-file.pdf'
|
||||
);
|
||||
});
|
||||
|
||||
it('handles PDF documents with partial timestamp patterns', () => {
|
||||
expect(formatDocumentLink('PDF:document_202412.pdf')).toBe(
|
||||
'document_202412.pdf'
|
||||
);
|
||||
expect(formatDocumentLink('PDF:file_123.pdf')).toBe('file_123.pdf');
|
||||
});
|
||||
|
||||
it('handles edge cases with timestamp pattern', () => {
|
||||
expect(
|
||||
formatDocumentLink('PDF:doc_20241227123045_final_20241227123045.pdf')
|
||||
).toBe('doc_20241227123045_final.pdf');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Regular URLs', () => {
|
||||
it('returns regular URLs unchanged', () => {
|
||||
expect(formatDocumentLink('https://example.com')).toBe(
|
||||
'https://example.com'
|
||||
);
|
||||
expect(formatDocumentLink('http://docs.example.com/api')).toBe(
|
||||
'http://docs.example.com/api'
|
||||
);
|
||||
expect(formatDocumentLink('https://github.com/user/repo')).toBe(
|
||||
'https://github.com/user/repo'
|
||||
);
|
||||
});
|
||||
|
||||
it('handles URLs with query parameters', () => {
|
||||
expect(formatDocumentLink('https://example.com?param=value')).toBe(
|
||||
'https://example.com?param=value'
|
||||
);
|
||||
expect(
|
||||
formatDocumentLink(
|
||||
'https://api.example.com/docs?version=v1&format=json'
|
||||
)
|
||||
).toBe('https://api.example.com/docs?version=v1&format=json');
|
||||
});
|
||||
|
||||
it('handles URLs with fragments', () => {
|
||||
expect(formatDocumentLink('https://example.com/docs#section1')).toBe(
|
||||
'https://example.com/docs#section1'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
import { LocalStorage } from '../localStorage';
|
||||
|
||||
// Mocking localStorage
|
||||
const localStorageMock = (() => {
|
||||
let store = {};
|
||||
return {
|
||||
getItem: key => store[key] || null,
|
||||
setItem: (key, value) => {
|
||||
store[key] = String(value);
|
||||
},
|
||||
removeItem: key => delete store[key],
|
||||
clear: () => {
|
||||
store = {};
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: localStorageMock,
|
||||
});
|
||||
|
||||
describe('LocalStorage utility', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('set and get methods', () => {
|
||||
LocalStorage.set('testKey', { a: 1 });
|
||||
expect(LocalStorage.get('testKey')).toEqual({ a: 1 });
|
||||
});
|
||||
|
||||
it('remove method', () => {
|
||||
LocalStorage.set('testKey', 'testValue');
|
||||
LocalStorage.remove('testKey');
|
||||
expect(LocalStorage.get('testKey')).toBeNull();
|
||||
});
|
||||
|
||||
it('updateJsonStore method', () => {
|
||||
LocalStorage.updateJsonStore('testStore', 'testKey', 'testValue');
|
||||
expect(LocalStorage.get('testStore')).toEqual({ testKey: 'testValue' });
|
||||
});
|
||||
|
||||
it('getFromJsonStore method', () => {
|
||||
LocalStorage.set('testStore', { testKey: 'testValue' });
|
||||
expect(LocalStorage.getFromJsonStore('testStore', 'testKey')).toBe(
|
||||
'testValue'
|
||||
);
|
||||
});
|
||||
|
||||
it('deleteFromJsonStore method', () => {
|
||||
LocalStorage.set('testStore', { testKey: 'testValue' });
|
||||
LocalStorage.deleteFromJsonStore('testStore', 'testKey');
|
||||
expect(LocalStorage.getFromJsonStore('testStore', 'testKey')).toBeNull();
|
||||
});
|
||||
|
||||
it('setFlag and getFlag methods', () => {
|
||||
const store = 'testStore';
|
||||
const accountId = '123';
|
||||
const key = 'flagKey';
|
||||
const expiry = 1000; // 1 second
|
||||
|
||||
// Set flag and verify it's set
|
||||
LocalStorage.setFlag(store, accountId, key, expiry);
|
||||
expect(LocalStorage.getFlag(store, accountId, key)).toBe(true);
|
||||
|
||||
// Wait for expiry and verify flag is not set
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
expect(LocalStorage.getFlag(store, accountId, key)).toBe(false);
|
||||
resolve();
|
||||
}, expiry + 100); // wait a bit more than expiry time to ensure the flag has expired
|
||||
});
|
||||
});
|
||||
|
||||
it('clearAll method', () => {
|
||||
LocalStorage.set('testKey1', 'testValue1');
|
||||
LocalStorage.set('testKey2', 'testValue2');
|
||||
LocalStorage.clearAll();
|
||||
expect(LocalStorage.get('testKey1')).toBeNull();
|
||||
expect(LocalStorage.get('testKey2')).toBeNull();
|
||||
});
|
||||
|
||||
it('set method with non-object value', () => {
|
||||
LocalStorage.set('testKey', 'testValue');
|
||||
expect(LocalStorage.get('testKey')).toBe('testValue');
|
||||
});
|
||||
|
||||
it('set and get methods with null value', () => {
|
||||
LocalStorage.set('testKey', null);
|
||||
expect(LocalStorage.get('testKey')).toBeNull();
|
||||
});
|
||||
|
||||
it('set and get methods with undefined value', () => {
|
||||
LocalStorage.set('testKey', undefined);
|
||||
expect(LocalStorage.get('testKey')).toBe('undefined');
|
||||
});
|
||||
|
||||
it('set and get methods with boolean value', () => {
|
||||
LocalStorage.set('testKey', true);
|
||||
expect(LocalStorage.get('testKey')).toBe(true);
|
||||
});
|
||||
|
||||
it('set and get methods with number value', () => {
|
||||
LocalStorage.set('testKey', 42);
|
||||
expect(LocalStorage.get('testKey')).toBe(42);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { emitter } from '../mitt';
|
||||
|
||||
describe('emitter', () => {
|
||||
it('should emit and listen to events correctly', () => {
|
||||
const mockCallback = vi.fn();
|
||||
|
||||
// Subscribe to an event
|
||||
emitter.on('event', mockCallback);
|
||||
|
||||
// Emit the event
|
||||
emitter.emit('event', 'data');
|
||||
|
||||
// Expect the callback to be called with the correct data
|
||||
expect(mockCallback).toHaveBeenCalledWith('data');
|
||||
|
||||
// Unsubscribe from the event
|
||||
emitter.off('event', mockCallback);
|
||||
|
||||
// Emit the event again
|
||||
emitter.emit('event', 'data');
|
||||
|
||||
// Expect the callback not to be called again
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { getMatchingLocale } from 'shared/helpers/portalHelper';
|
||||
|
||||
describe('portalHelper - getMatchingLocale', () => {
|
||||
it('returns exact match when present', () => {
|
||||
const result = getMatchingLocale('fr', ['en', 'fr']);
|
||||
expect(result).toBe('fr');
|
||||
});
|
||||
|
||||
it('returns base language match when exact variant not present', () => {
|
||||
const result = getMatchingLocale('fr_CA', ['en', 'fr']);
|
||||
expect(result).toBe('fr');
|
||||
});
|
||||
|
||||
it('returns variant match when base language not present', () => {
|
||||
const result = getMatchingLocale('fr', ['en', 'fr_BE']);
|
||||
expect(result).toBe('fr_BE');
|
||||
});
|
||||
|
||||
it('returns null when no match found', () => {
|
||||
const result = getMatchingLocale('de', ['en', 'fr']);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for invalid inputs', () => {
|
||||
expect(getMatchingLocale('', [])).toBeNull();
|
||||
expect(getMatchingLocale(null, null)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { sanitizeLabel } from '../sanitizeData';
|
||||
|
||||
describe('sanitizeLabel', () => {
|
||||
it('should return an empty string when given an empty string', () => {
|
||||
const label = '';
|
||||
const sanitizedLabel = sanitizeLabel(label);
|
||||
expect(sanitizedLabel).toEqual('');
|
||||
});
|
||||
|
||||
it('should remove leading and trailing whitespace', () => {
|
||||
const label = ' My Label ';
|
||||
const sanitizedLabel = sanitizeLabel(label);
|
||||
expect(sanitizedLabel).toEqual('my-label');
|
||||
});
|
||||
|
||||
it('should convert all characters to lowercase', () => {
|
||||
const label = 'My Label';
|
||||
const sanitizedLabel = sanitizeLabel(label);
|
||||
expect(sanitizedLabel).toEqual('my-label');
|
||||
});
|
||||
|
||||
it('should replace spaces with hyphens', () => {
|
||||
const label = 'My Label 123';
|
||||
const sanitizedLabel = sanitizeLabel(label);
|
||||
expect(sanitizedLabel).toEqual('my-label-123');
|
||||
});
|
||||
|
||||
it('should remove any characters that are not alphanumeric, underscore, or hyphen', () => {
|
||||
const label = 'My_Label!123';
|
||||
const sanitizedLabel = sanitizeLabel(label);
|
||||
expect(sanitizedLabel).toEqual('my_label123');
|
||||
});
|
||||
|
||||
it('should handle null and undefined input', () => {
|
||||
const nullLabel = null;
|
||||
const undefinedLabel = undefined;
|
||||
|
||||
// @ts-ignore - intentionally passing null and undefined to test
|
||||
const sanitizedNullLabel = sanitizeLabel(nullLabel);
|
||||
const sanitizedUndefinedLabel = sanitizeLabel(undefinedLabel);
|
||||
expect(sanitizedNullLabel).toEqual('');
|
||||
expect(sanitizedUndefinedLabel).toEqual('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
import SessionStorage from '../sessionStorage';
|
||||
|
||||
// Mocking sessionStorage
|
||||
const sessionStorageMock = (() => {
|
||||
let store = {};
|
||||
return {
|
||||
getItem: key => store[key] || null,
|
||||
setItem: (key, value) => {
|
||||
store[key] = String(value);
|
||||
},
|
||||
removeItem: key => delete store[key],
|
||||
clear: () => {
|
||||
store = {};
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
Object.defineProperty(window, 'sessionStorage', {
|
||||
value: sessionStorageMock,
|
||||
});
|
||||
|
||||
describe('SessionStorage utility', () => {
|
||||
beforeEach(() => {
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
||||
describe('clearAll method', () => {
|
||||
it('should clear all items from sessionStorage', () => {
|
||||
sessionStorage.setItem('testKey1', 'testValue1');
|
||||
sessionStorage.setItem('testKey2', 'testValue2');
|
||||
|
||||
SessionStorage.clearAll();
|
||||
|
||||
expect(sessionStorage.getItem('testKey1')).toBeNull();
|
||||
expect(sessionStorage.getItem('testKey2')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('get method', () => {
|
||||
it('should retrieve and parse JSON values correctly', () => {
|
||||
const testObject = { a: 1, b: 'test' };
|
||||
sessionStorage.setItem('testKey', JSON.stringify(testObject));
|
||||
|
||||
expect(SessionStorage.get('testKey')).toEqual(testObject);
|
||||
});
|
||||
|
||||
it('should return null for non-existent keys', () => {
|
||||
expect(SessionStorage.get('nonExistentKey')).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle non-JSON values by returning the raw value', () => {
|
||||
sessionStorage.setItem('testKey', 'plain string value');
|
||||
|
||||
expect(SessionStorage.get('testKey')).toBe('plain string value');
|
||||
});
|
||||
|
||||
it('should handle malformed JSON gracefully', () => {
|
||||
sessionStorage.setItem('testKey', '{malformed:json}');
|
||||
|
||||
expect(SessionStorage.get('testKey')).toBe('{malformed:json}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('set method', () => {
|
||||
it('should store object values as JSON strings', () => {
|
||||
const testObject = { a: 1, b: 'test' };
|
||||
SessionStorage.set('testKey', testObject);
|
||||
|
||||
expect(sessionStorage.getItem('testKey')).toBe(
|
||||
JSON.stringify(testObject)
|
||||
);
|
||||
});
|
||||
|
||||
it('should store primitive values directly', () => {
|
||||
SessionStorage.set('stringKey', 'test string');
|
||||
expect(sessionStorage.getItem('stringKey')).toBe('test string');
|
||||
|
||||
SessionStorage.set('numberKey', 42);
|
||||
expect(sessionStorage.getItem('numberKey')).toBe('42');
|
||||
|
||||
SessionStorage.set('booleanKey', true);
|
||||
expect(sessionStorage.getItem('booleanKey')).toBe('true');
|
||||
});
|
||||
|
||||
it('should handle null values', () => {
|
||||
SessionStorage.set('nullKey', null);
|
||||
|
||||
expect(sessionStorage.getItem('nullKey')).toBe('null');
|
||||
expect(SessionStorage.get('nullKey')).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle undefined values', () => {
|
||||
SessionStorage.set('undefinedKey', undefined);
|
||||
|
||||
expect(sessionStorage.getItem('undefinedKey')).toBe('undefined');
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove method', () => {
|
||||
it('should remove an item from sessionStorage', () => {
|
||||
SessionStorage.set('testKey', 'testValue');
|
||||
expect(SessionStorage.get('testKey')).toBe('testValue');
|
||||
|
||||
SessionStorage.remove('testKey');
|
||||
|
||||
expect(SessionStorage.get('testKey')).toBeNull();
|
||||
});
|
||||
|
||||
it('should do nothing when removing a non-existent key', () => {
|
||||
expect(() => {
|
||||
SessionStorage.remove('nonExistentKey');
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration of methods', () => {
|
||||
it('should set, get, and remove values correctly', () => {
|
||||
SessionStorage.set('testKey', { value: 'test' });
|
||||
|
||||
expect(SessionStorage.get('testKey')).toEqual({ value: 'test' });
|
||||
|
||||
SessionStorage.remove('testKey');
|
||||
expect(SessionStorage.get('testKey')).toBeNull();
|
||||
});
|
||||
|
||||
it('should correctly handle impersonation flag (common use case)', () => {
|
||||
SessionStorage.set('impersonationUser', true);
|
||||
|
||||
expect(SessionStorage.get('impersonationUser')).toBe(true);
|
||||
|
||||
expect(sessionStorage.getItem('impersonationUser')).toBe('true');
|
||||
|
||||
SessionStorage.remove('impersonationUser');
|
||||
expect(SessionStorage.get('impersonationUser')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,239 @@
|
||||
import {
|
||||
messageStamp,
|
||||
messageTimestamp,
|
||||
dynamicTime,
|
||||
dateFormat,
|
||||
shortTimestamp,
|
||||
getDayDifferenceFromNow,
|
||||
hasOneDayPassed,
|
||||
} from 'shared/helpers/timeHelper';
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.TZ = 'UTC';
|
||||
vi.useFakeTimers('modern');
|
||||
const mockDate = new Date(Date.UTC(2023, 4, 5));
|
||||
vi.setSystemTime(mockDate);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('#messageStamp', () => {
|
||||
it('returns correct value', () => {
|
||||
expect(messageStamp(1612971343)).toEqual('3:35 PM');
|
||||
expect(messageStamp(1612971343, 'LLL d, h:mm a')).toEqual(
|
||||
'Feb 10, 3:35 PM'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#messageTimestamp', () => {
|
||||
it('should return the message date in the specified format if the message was sent in the current year', () => {
|
||||
expect(messageTimestamp(1680777464)).toEqual('Apr 6, 2023');
|
||||
});
|
||||
it('should return the message date and time in a different format if the message was sent in a different year', () => {
|
||||
expect(messageTimestamp(1612971343)).toEqual('Feb 10 2021, 3:35 PM');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#dynamicTime', () => {
|
||||
it('returns correct value', () => {
|
||||
Date.now = vi.fn(() => new Date(Date.UTC(2023, 1, 14)).valueOf());
|
||||
expect(dynamicTime(1612971343)).toEqual('about 2 years ago');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#dateFormat', () => {
|
||||
it('returns correct value', () => {
|
||||
expect(dateFormat(1612971343)).toEqual('Feb 10, 2021');
|
||||
expect(dateFormat(1612971343, 'LLL d, yyyy')).toEqual('Feb 10, 2021');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#shortTimestamp', () => {
|
||||
// Test cases when withAgo is false or not provided
|
||||
it('returns correct value without ago', () => {
|
||||
expect(shortTimestamp('less than a minute ago')).toEqual('now');
|
||||
expect(shortTimestamp('1 minute ago')).toEqual('1m');
|
||||
expect(shortTimestamp('12 minutes ago')).toEqual('12m');
|
||||
expect(shortTimestamp('a minute ago')).toEqual('1m');
|
||||
expect(shortTimestamp('an hour ago')).toEqual('1h');
|
||||
expect(shortTimestamp('1 hour ago')).toEqual('1h');
|
||||
expect(shortTimestamp('2 hours ago')).toEqual('2h');
|
||||
expect(shortTimestamp('1 day ago')).toEqual('1d');
|
||||
expect(shortTimestamp('a day ago')).toEqual('1d');
|
||||
expect(shortTimestamp('3 days ago')).toEqual('3d');
|
||||
expect(shortTimestamp('a month ago')).toEqual('1mo');
|
||||
expect(shortTimestamp('1 month ago')).toEqual('1mo');
|
||||
expect(shortTimestamp('2 months ago')).toEqual('2mo');
|
||||
expect(shortTimestamp('a year ago')).toEqual('1y');
|
||||
expect(shortTimestamp('1 year ago')).toEqual('1y');
|
||||
expect(shortTimestamp('4 years ago')).toEqual('4y');
|
||||
});
|
||||
|
||||
// Test cases when withAgo is true
|
||||
it('returns correct value with ago', () => {
|
||||
expect(shortTimestamp('less than a minute ago', true)).toEqual('now');
|
||||
expect(shortTimestamp('1 minute ago', true)).toEqual('1m ago');
|
||||
expect(shortTimestamp('12 minutes ago', true)).toEqual('12m ago');
|
||||
expect(shortTimestamp('a minute ago', true)).toEqual('1m ago');
|
||||
expect(shortTimestamp('an hour ago', true)).toEqual('1h ago');
|
||||
expect(shortTimestamp('1 hour ago', true)).toEqual('1h ago');
|
||||
expect(shortTimestamp('2 hours ago', true)).toEqual('2h ago');
|
||||
expect(shortTimestamp('1 day ago', true)).toEqual('1d ago');
|
||||
expect(shortTimestamp('a day ago', true)).toEqual('1d ago');
|
||||
expect(shortTimestamp('3 days ago', true)).toEqual('3d ago');
|
||||
expect(shortTimestamp('a month ago', true)).toEqual('1mo ago');
|
||||
expect(shortTimestamp('1 month ago', true)).toEqual('1mo ago');
|
||||
expect(shortTimestamp('2 months ago', true)).toEqual('2mo ago');
|
||||
expect(shortTimestamp('a year ago', true)).toEqual('1y ago');
|
||||
expect(shortTimestamp('1 year ago', true)).toEqual('1y ago');
|
||||
expect(shortTimestamp('4 years ago', true)).toEqual('4y ago');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getDayDifferenceFromNow', () => {
|
||||
it('returns 0 for timestamps from today', () => {
|
||||
// Mock current date: May 5, 2023
|
||||
const now = new Date(Date.UTC(2023, 4, 5, 12, 0, 0)); // 12:00 PM
|
||||
const todayTimestamp = Math.floor(now.getTime() / 1000); // Same day
|
||||
|
||||
expect(getDayDifferenceFromNow(now, todayTimestamp)).toEqual(0);
|
||||
});
|
||||
|
||||
it('returns 2 for timestamps from 2 days ago', () => {
|
||||
const now = new Date(Date.UTC(2023, 4, 5, 12, 0, 0)); // May 5, 2023
|
||||
const twoDaysAgoTimestamp = Math.floor(
|
||||
new Date(Date.UTC(2023, 4, 3, 10, 0, 0)).getTime() / 1000
|
||||
); // May 3, 2023
|
||||
|
||||
expect(getDayDifferenceFromNow(now, twoDaysAgoTimestamp)).toEqual(2);
|
||||
});
|
||||
|
||||
it('returns 7 for timestamps from a week ago', () => {
|
||||
const now = new Date(Date.UTC(2023, 4, 5, 12, 0, 0)); // May 5, 2023
|
||||
const weekAgoTimestamp = Math.floor(
|
||||
new Date(Date.UTC(2023, 3, 28, 8, 0, 0)).getTime() / 1000
|
||||
); // April 28, 2023
|
||||
|
||||
expect(getDayDifferenceFromNow(now, weekAgoTimestamp)).toEqual(7);
|
||||
});
|
||||
|
||||
it('returns 30 for timestamps from a month ago', () => {
|
||||
const now = new Date(Date.UTC(2023, 4, 5, 12, 0, 0)); // May 5, 2023
|
||||
const monthAgoTimestamp = Math.floor(
|
||||
new Date(Date.UTC(2023, 3, 5, 12, 0, 0)).getTime() / 1000
|
||||
); // April 5, 2023
|
||||
|
||||
expect(getDayDifferenceFromNow(now, monthAgoTimestamp)).toEqual(30);
|
||||
});
|
||||
|
||||
it('handles edge case with different times on same day', () => {
|
||||
const now = new Date(Date.UTC(2023, 4, 5, 23, 59, 59)); // May 5, 2023 11:59:59 PM
|
||||
const morningTimestamp = Math.floor(
|
||||
new Date(Date.UTC(2023, 4, 5, 0, 0, 1)).getTime() / 1000
|
||||
); // May 5, 2023 12:00:01 AM
|
||||
|
||||
expect(getDayDifferenceFromNow(now, morningTimestamp)).toEqual(0);
|
||||
});
|
||||
|
||||
it('handles cross-month boundaries correctly', () => {
|
||||
const now = new Date(Date.UTC(2023, 4, 1, 12, 0, 0)); // May 1, 2023
|
||||
const lastMonthTimestamp = Math.floor(
|
||||
new Date(Date.UTC(2023, 3, 30, 12, 0, 0)).getTime() / 1000
|
||||
); // April 30, 2023
|
||||
|
||||
expect(getDayDifferenceFromNow(now, lastMonthTimestamp)).toEqual(1);
|
||||
});
|
||||
|
||||
it('handles cross-year boundaries correctly', () => {
|
||||
const now = new Date(Date.UTC(2023, 0, 2, 12, 0, 0)); // January 2, 2023
|
||||
const lastYearTimestamp = Math.floor(
|
||||
new Date(Date.UTC(2022, 11, 31, 12, 0, 0)).getTime() / 1000
|
||||
); // December 31, 2022
|
||||
|
||||
expect(getDayDifferenceFromNow(now, lastYearTimestamp)).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#hasOneDayPassed', () => {
|
||||
beforeEach(() => {
|
||||
// Mock current date: May 5, 2023, 12:00 PM UTC (1683288000)
|
||||
const mockDate = new Date(1683288000 * 1000);
|
||||
vi.setSystemTime(mockDate);
|
||||
});
|
||||
|
||||
it('returns false for timestamps from today', () => {
|
||||
// Same day, different time - May 5, 2023 8:00 AM UTC
|
||||
const todayTimestamp = 1683273600;
|
||||
|
||||
expect(hasOneDayPassed(todayTimestamp)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for timestamps from yesterday (less than 24 hours)', () => {
|
||||
// Yesterday but less than 24 hours ago - May 4, 2023 6:00 PM UTC (18 hours ago)
|
||||
const yesterdayTimestamp = 1683230400;
|
||||
|
||||
expect(hasOneDayPassed(yesterdayTimestamp)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for timestamps from exactly 1 day ago', () => {
|
||||
// Exactly 24 hours ago - May 4, 2023 12:00 PM UTC
|
||||
const oneDayAgoTimestamp = 1683201600;
|
||||
|
||||
expect(hasOneDayPassed(oneDayAgoTimestamp)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for timestamps from more than 1 day ago', () => {
|
||||
// 2 days ago - May 3, 2023 10:00 AM UTC
|
||||
const twoDaysAgoTimestamp = 1683108000;
|
||||
|
||||
expect(hasOneDayPassed(twoDaysAgoTimestamp)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for timestamps from a week ago', () => {
|
||||
// 7 days ago - April 28, 2023 8:00 AM UTC
|
||||
const weekAgoTimestamp = 1682668800;
|
||||
|
||||
expect(hasOneDayPassed(weekAgoTimestamp)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for null timestamp (defensive check)', () => {
|
||||
expect(hasOneDayPassed(null)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for undefined timestamp (defensive check)', () => {
|
||||
expect(hasOneDayPassed(undefined)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for zero timestamp (defensive check)', () => {
|
||||
expect(hasOneDayPassed(0)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for empty string timestamp (defensive check)', () => {
|
||||
expect(hasOneDayPassed('')).toBe(true);
|
||||
});
|
||||
|
||||
it('handles cross-month boundaries correctly', () => {
|
||||
// Set current time to May 1, 2023 12:00 PM UTC (1682942400)
|
||||
const mayFirst = new Date(1682942400 * 1000);
|
||||
vi.setSystemTime(mayFirst);
|
||||
|
||||
// April 29, 2023 12:00 PM UTC (1682769600) - 2 days ago, crossing month boundary
|
||||
const crossMonthTimestamp = 1682769600;
|
||||
|
||||
expect(hasOneDayPassed(crossMonthTimestamp)).toBe(true);
|
||||
});
|
||||
|
||||
it('handles cross-year boundaries correctly', () => {
|
||||
// Set current time to January 2, 2023 12:00 PM UTC (1672660800)
|
||||
const newYear = new Date(1672660800 * 1000);
|
||||
vi.setSystemTime(newYear);
|
||||
|
||||
// December 30, 2022 12:00 PM UTC (1672401600) - 3 days ago, crossing year boundary
|
||||
const crossYearTimestamp = 1672401600;
|
||||
|
||||
expect(hasOneDayPassed(crossYearTimestamp)).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user