Restructure omni services and add Chatwoot research snapshot

This commit is contained in:
Ruslan Bakiev
2026-02-21 11:11:27 +07:00
parent edea7a0034
commit b73babbbf6
7732 changed files with 978203 additions and 32 deletions

View File

@@ -0,0 +1,96 @@
import { useBranding } from '../useBranding';
import { useMapGetter } from 'dashboard/composables/store.js';
// Mock the store composable
vi.mock('dashboard/composables/store.js', () => ({
useMapGetter: vi.fn(),
}));
describe('useBranding', () => {
let mockGlobalConfig;
beforeEach(() => {
mockGlobalConfig = {
value: {
installationName: 'MyCompany',
},
};
useMapGetter.mockReturnValue(mockGlobalConfig);
});
afterEach(() => {
vi.clearAllMocks();
});
describe('replaceInstallationName', () => {
it('should replace "Chatwoot" with installation name when both text and installation name are provided', () => {
const { replaceInstallationName } = useBranding();
const result = replaceInstallationName('Welcome to Chatwoot');
expect(result).toBe('Welcome to MyCompany');
});
it('should replace multiple occurrences of "Chatwoot"', () => {
const { replaceInstallationName } = useBranding();
const result = replaceInstallationName(
'Chatwoot is great! Use Chatwoot today.'
);
expect(result).toBe('MyCompany is great! Use MyCompany today.');
});
it('should return original text when installation name is not provided', () => {
mockGlobalConfig.value = {};
const { replaceInstallationName } = useBranding();
const result = replaceInstallationName('Welcome to Chatwoot');
expect(result).toBe('Welcome to Chatwoot');
});
it('should return original text when globalConfig is not available', () => {
mockGlobalConfig.value = undefined;
const { replaceInstallationName } = useBranding();
const result = replaceInstallationName('Welcome to Chatwoot');
expect(result).toBe('Welcome to Chatwoot');
});
it('should return original text when text is empty or null', () => {
const { replaceInstallationName } = useBranding();
expect(replaceInstallationName('')).toBe('');
expect(replaceInstallationName(null)).toBe(null);
expect(replaceInstallationName(undefined)).toBe(undefined);
});
it('should handle text without "Chatwoot" gracefully', () => {
const { replaceInstallationName } = useBranding();
const result = replaceInstallationName('Welcome to our platform');
expect(result).toBe('Welcome to our platform');
});
it('should be case-sensitive for "Chatwoot"', () => {
const { replaceInstallationName } = useBranding();
const result = replaceInstallationName(
'Welcome to chatwoot and CHATWOOT'
);
expect(result).toBe('Welcome to chatwoot and CHATWOOT');
});
it('should handle special characters in installation name', () => {
mockGlobalConfig.value = {
installationName: 'My-Company & Co.',
};
const { replaceInstallationName } = useBranding();
const result = replaceInstallationName('Welcome to Chatwoot');
expect(result).toBe('Welcome to My-Company & Co.');
});
});
});

View File

@@ -0,0 +1,130 @@
import { nextTick } from 'vue';
import { useExpandableContent } from '../useExpandableContent';
// Mock VueUse composables
vi.mock('@vueuse/core', () => ({
useToggle: vi.fn(initialValue => {
let value = initialValue;
const toggle = newValue => {
value = newValue !== undefined ? newValue : !value;
};
return [{ value }, toggle];
}),
useResizeObserver: vi.fn((element, callback) => {
// Store callback for manual triggering in tests
if (element.value) {
callback();
}
}),
}));
describe('useExpandableContent', () => {
let originalGetComputedStyle;
beforeEach(() => {
// Mock window.getComputedStyle
originalGetComputedStyle = window.getComputedStyle;
window.getComputedStyle = vi.fn(() => ({
lineHeight: '20px',
}));
});
afterEach(() => {
window.getComputedStyle = originalGetComputedStyle;
vi.clearAllMocks();
});
it('returns expected properties', () => {
const result = useExpandableContent();
expect(result).toHaveProperty('contentElement');
expect(result).toHaveProperty('isExpanded');
expect(result).toHaveProperty('needsToggle');
expect(result).toHaveProperty('toggleExpanded');
expect(result).toHaveProperty('checkOverflow');
});
it('initializes with default values', () => {
const { isExpanded, needsToggle } = useExpandableContent();
expect(isExpanded.value).toBe(false);
expect(needsToggle.value).toBe(false);
});
it('checkOverflow sets needsToggle to true when content overflows', async () => {
const { contentElement, needsToggle, checkOverflow } =
useExpandableContent();
// Mock element with overflow
contentElement.value = {
scrollHeight: 100, // Much larger than 2 lines (40px)
};
checkOverflow();
await nextTick();
expect(needsToggle.value).toBe(true);
});
it('checkOverflow sets needsToggle to false when content fits', async () => {
const { contentElement, needsToggle, checkOverflow } =
useExpandableContent();
// Mock element without overflow
contentElement.value = {
scrollHeight: 30, // Less than 2 lines (40px)
};
checkOverflow();
await nextTick();
expect(needsToggle.value).toBe(false);
});
it('respects custom maxLines option', async () => {
const { contentElement, needsToggle, checkOverflow } = useExpandableContent(
{
maxLines: 3,
}
);
// Mock element that fits in 3 lines but not 2
contentElement.value = {
scrollHeight: 50, // Fits in 3 lines (60px) but not 2 lines (40px)
};
checkOverflow();
await nextTick();
expect(needsToggle.value).toBe(false);
});
it('uses defaultLineHeight when computed style is unavailable', async () => {
window.getComputedStyle = vi.fn(() => ({
lineHeight: 'normal', // Not a valid number
}));
const { contentElement, needsToggle, checkOverflow } = useExpandableContent(
{
defaultLineHeight: 16,
}
);
// Mock element that overflows with 16px line height (32px max)
contentElement.value = {
scrollHeight: 40,
};
checkOverflow();
await nextTick();
expect(needsToggle.value).toBe(true);
});
it('handles null contentElement gracefully', () => {
const { checkOverflow } = useExpandableContent();
// Should not throw when contentElement is null
expect(() => checkOverflow()).not.toThrow();
});
});

View File

@@ -0,0 +1,145 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { useFilter } from '../useFilter';
import { useStore } from 'dashboard/composables/store';
import { useI18n } from 'vue-i18n';
// Mock the dependencies
vi.mock('dashboard/composables/store');
vi.mock('vue-i18n');
describe('useFilter', () => {
// Setup mocks
beforeEach(() => {
vi.mocked(useStore).mockReturnValue({
getters: {
'attributes/getAttributesByModel': vi.fn(),
},
});
vi.mocked(useI18n).mockReturnValue({
t: vi.fn(key => key),
});
});
it('should return the correct functions', () => {
const {
setFilterAttributes,
initializeStatusAndAssigneeFilterToModal,
initializeInboxTeamAndLabelFilterToModal,
} = useFilter({ filteri18nKey: 'TEST', attributeModel: 'conversation' });
expect(setFilterAttributes).toBeDefined();
expect(initializeStatusAndAssigneeFilterToModal).toBeDefined();
expect(initializeInboxTeamAndLabelFilterToModal).toBeDefined();
});
describe('setFilterAttributes', () => {
it('should return filterGroups and filterTypes', () => {
const mockAttributes = [
{
attribute_key: 'test_key',
attribute_display_name: 'Test Name',
attribute_display_type: 'text',
},
];
vi.mocked(useStore)().getters[
'attributes/getAttributesByModel'
].mockReturnValue(mockAttributes);
const { setFilterAttributes } = useFilter({
filteri18nKey: 'TEST',
attributeModel: 'conversation',
});
const result = setFilterAttributes();
expect(result).toHaveProperty('filterGroups');
expect(result).toHaveProperty('filterTypes');
expect(result.filterGroups.length).toBeGreaterThan(0);
expect(result.filterTypes.length).toBeGreaterThan(0);
});
});
describe('initializeStatusAndAssigneeFilterToModal', () => {
it('should return status filter when activeStatus is provided', () => {
const { initializeStatusAndAssigneeFilterToModal } = useFilter({
filteri18nKey: 'TEST',
attributeModel: 'conversation',
});
const result = initializeStatusAndAssigneeFilterToModal('open', {}, '');
expect(result).toEqual({
attribute_key: 'status',
attribute_model: 'standard',
filter_operator: 'equal_to',
values: [
{ id: 'open', name: 'CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.open.TEXT' },
],
query_operator: 'and',
custom_attribute_type: '',
});
});
it('should return null when no active filters', () => {
const { initializeStatusAndAssigneeFilterToModal } = useFilter({
filteri18nKey: 'TEST',
attributeModel: 'conversation',
});
const result = initializeStatusAndAssigneeFilterToModal('', {}, '');
expect(result).toBeNull();
});
});
describe('initializeInboxTeamAndLabelFilterToModal', () => {
it('should return filters for inbox, team, and label when provided', () => {
const { initializeInboxTeamAndLabelFilterToModal } = useFilter({
filteri18nKey: 'TEST',
attributeModel: 'conversation',
});
const result = initializeInboxTeamAndLabelFilterToModal(
1,
{ name: 'Inbox 1' },
2,
[{ id: 2, name: 'Team 2' }],
'Label 1'
);
expect(result).toHaveLength(3);
expect(result[0]).toHaveProperty('attribute_key', 'inbox_id');
expect(result[1]).toHaveProperty('attribute_key', 'team_id');
expect(result[2]).toHaveProperty('attribute_key', 'labels');
});
it('should return empty array when no filters are provided', () => {
const { initializeInboxTeamAndLabelFilterToModal } = useFilter({
filteri18nKey: 'TEST',
attributeModel: 'conversation',
});
const result = initializeInboxTeamAndLabelFilterToModal(
null,
null,
null,
null,
null
);
expect(result).toEqual([]);
});
it('should return only inbox filter when only inbox is provided', () => {
const { initializeInboxTeamAndLabelFilterToModal } = useFilter({
filteri18nKey: 'TEST',
attributeModel: 'conversation',
});
const result = initializeInboxTeamAndLabelFilterToModal(
1,
{ name: 'Inbox 1' },
null,
null,
null
);
expect(result).toHaveLength(1);
expect(result[0]).toHaveProperty('attribute_key', 'inbox_id');
});
});
});

View File

@@ -0,0 +1,188 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { useLocale } from '../useLocale';
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
vi.mock('vue-i18n');
describe('useLocale', () => {
beforeEach(() => {
vi.mocked(useI18n).mockReturnValue({
locale: ref('en-US'),
});
});
describe('resolvedLocale', () => {
it('should return normalized locale for valid hyphen-based tags', () => {
const mockLocale = ref('en-US');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { resolvedLocale } = useLocale();
expect(resolvedLocale.value).toBe('en-US');
});
it('should normalize underscore-based locale tags to hyphens', () => {
const mockLocale = ref('pt_BR');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { resolvedLocale } = useLocale();
// Should normalize pt_BR to pt-BR
expect(resolvedLocale.value).toMatch(/^pt(-BR)?$/);
});
it('should normalize zh_CN to zh-CN or fall back to zh', () => {
const mockLocale = ref('zh_CN');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { resolvedLocale } = useLocale();
// Should normalize zh_CN to zh-CN or fall back to zh
expect(resolvedLocale.value).toMatch(/^zh(-CN)?$/);
});
it('should normalize en_US to en-US or fall back to en', () => {
const mockLocale = ref('en_US');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { resolvedLocale } = useLocale();
// Should normalize en_US to en-US or fall back to en
expect(resolvedLocale.value).toMatch(/^en(-US)?$/);
});
it('should fall back to base language when specific locale not supported', () => {
// Use a specific locale that might not be fully supported
const mockLocale = ref('pt-BR');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { resolvedLocale } = useLocale();
// Should return either pt-BR or pt (base language)
expect(resolvedLocale.value).toMatch(/^pt(-BR)?$/);
});
it('should fall back to English for completely unsupported locales', () => {
const mockLocale = ref('xx-YY');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { resolvedLocale } = useLocale();
// Should fall back to 'en'
expect(resolvedLocale.value).toBe('en');
});
it('should handle null locale gracefully', () => {
const mockLocale = ref(null);
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { resolvedLocale } = useLocale();
// Should fall back to 'en'
expect(resolvedLocale.value).toBe('en');
});
it('should handle undefined locale gracefully', () => {
const mockLocale = ref(undefined);
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { resolvedLocale } = useLocale();
// Should fall back to 'en'
expect(resolvedLocale.value).toBe('en');
});
it('should handle base language code without region', () => {
const mockLocale = ref('pt');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { resolvedLocale } = useLocale();
// Should work with base language
expect(resolvedLocale.value).toBe('pt');
});
it('should handle multiple underscores in locale tag', () => {
const mockLocale = ref('zh_Hans_CN');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { resolvedLocale } = useLocale();
// Should normalize all underscores to hyphens
expect(resolvedLocale.value).toMatch(/^zh(-Hans-CN|-Hans|-CN)?$/);
});
it('should be reactive to locale changes', () => {
const mockLocale = ref('en-US');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { resolvedLocale } = useLocale();
expect(resolvedLocale.value).toBe('en-US');
// Change locale
mockLocale.value = 'pt_BR';
// Should update reactively
expect(resolvedLocale.value).toMatch(/^pt(-BR)?$/);
});
it('should work with common locales', () => {
const testCases = [
{ input: 'de-DE', expected: /^de(-DE)?$/ },
{ input: 'fr-FR', expected: /^fr(-FR)?$/ },
{ input: 'es-ES', expected: /^es(-ES)?$/ },
{ input: 'ja-JP', expected: /^ja(-JP)?$/ },
{ input: 'ko-KR', expected: /^ko(-KR)?$/ },
{ input: 'ar-SA', expected: /^ar(-SA)?$/ },
{ input: 'hi-IN', expected: /^hi(-IN)?$/ },
{ input: 'ru-RU', expected: /^ru(-RU)?$/ },
{ input: 'tr-TR', expected: /^tr(-TR)?$/ },
];
testCases.forEach(({ input, expected }) => {
const mockLocale = ref(input);
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { resolvedLocale } = useLocale();
expect(resolvedLocale.value).toMatch(expected);
});
});
});
describe('locale (raw)', () => {
it('should expose raw locale value', () => {
const mockLocale = ref('pt_BR');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { locale } = useLocale();
// Raw locale should be unchanged
expect(locale.value).toBe('pt_BR');
});
it('should be reactive', () => {
const mockLocale = ref('en-US');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { locale } = useLocale();
expect(locale.value).toBe('en-US');
mockLocale.value = 'pt-BR';
expect(locale.value).toBe('pt-BR');
});
});
describe('Intl API compatibility', () => {
it('should work with Intl.NumberFormat', () => {
const mockLocale = ref('pt_BR');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { resolvedLocale } = useLocale();
// Should not throw error
expect(() => {
new Intl.NumberFormat(resolvedLocale.value).format(1234567);
}).not.toThrow();
});
it('should work with Intl.DateTimeFormat', () => {
const mockLocale = ref('zh_CN');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { resolvedLocale } = useLocale();
// Should not throw error
expect(() => {
new Intl.DateTimeFormat(resolvedLocale.value).format(new Date());
}).not.toThrow();
});
it('should work with Intl.Collator', () => {
const mockLocale = ref('en_US');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { resolvedLocale } = useLocale();
// Should not throw error
expect(() => {
new Intl.Collator(resolvedLocale.value).compare('a', 'b');
}).not.toThrow();
});
});
});

View File

@@ -0,0 +1,91 @@
import { useMessageFormatter } from '../useMessageFormatter';
describe('useMessageFormatter', () => {
let messageFormatter;
beforeEach(() => {
messageFormatter = useMessageFormatter();
});
describe('formatMessage', () => {
it('should format a regular message correctly', () => {
const message = 'This is a [test](https://example.com) message';
const result = messageFormatter.formatMessage(message, false, false);
expect(result).toContain('<a href="https://example.com"');
expect(result).toContain('class="link"');
});
it('should format a tweet correctly', () => {
const message = '@user #hashtag';
const result = messageFormatter.formatMessage(message, true, false);
expect(result).toContain('<a href="http://twitter.com/user"');
expect(result).toContain('<a href="https://twitter.com/hashtag/hashtag"');
});
it('should not format mentions and hashtags for private notes', () => {
const message = '@user #hashtag';
const result = messageFormatter.formatMessage(message, false, true);
expect(result).not.toContain('<a href="http://twitter.com/user"');
expect(result).not.toContain(
'<a href="https://twitter.com/hashtag/hashtag"'
);
});
it('should disable link formatting when linkify is false', () => {
const message = 'Check https://example.com and {{user.id}}';
const result = messageFormatter.formatMessage(
message,
false,
false,
false
);
expect(result).not.toContain('<a href="https://example.com"');
expect(result).toContain('{{user.id}}');
});
});
describe('truncateMessage', () => {
it('should not truncate short messages', () => {
const message = 'Short message';
const result = messageFormatter.truncateMessage(message);
expect(result).toBe(message);
});
it('should truncate long messages', () => {
const message = 'A'.repeat(150);
const result = messageFormatter.truncateMessage(message);
expect(result.length).toBe(100);
expect(result.endsWith('...')).toBe(true);
});
});
describe('highlightContent', () => {
it('should highlight search term in content', () => {
const content = 'This is a test message';
const searchTerm = 'test';
const highlightClass = 'highlight';
const result = messageFormatter.highlightContent(
content,
searchTerm,
highlightClass
);
expect(result.trim()).toBe(
'This is a <span class="highlight">test</span> message'
);
});
it('should handle special characters in search term', () => {
const content = 'This (message) contains [special] characters';
const searchTerm = '(message)';
const highlightClass = 'highlight';
const result = messageFormatter.highlightContent(
content,
searchTerm,
highlightClass
);
expect(result.trim()).toBe(
'This <span class="highlight">(message)</span> contains [special] characters'
);
});
});
});

View File

@@ -0,0 +1,364 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { useNumberFormatter } from '../useNumberFormatter';
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
vi.mock('vue-i18n');
describe('useNumberFormatter', () => {
beforeEach(() => {
vi.mocked(useI18n).mockReturnValue({
locale: ref('en-US'),
});
});
describe('formatCompactNumber', () => {
it('should return exact numbers for values under 1,000', () => {
const { formatCompactNumber } = useNumberFormatter();
expect(formatCompactNumber(0)).toBe('0');
expect(formatCompactNumber(1)).toBe('1');
expect(formatCompactNumber(42)).toBe('42');
expect(formatCompactNumber(999)).toBe('999');
});
it('should return "Xk" for exact thousands and "Xk+" for values with remainder', () => {
const { formatCompactNumber } = useNumberFormatter();
expect(formatCompactNumber(1000)).toBe('1k');
expect(formatCompactNumber(1020)).toBe('1k+');
expect(formatCompactNumber(1500)).toBe('1k+');
expect(formatCompactNumber(1999)).toBe('1k+');
expect(formatCompactNumber(2000)).toBe('2k');
expect(formatCompactNumber(15000)).toBe('15k');
expect(formatCompactNumber(15500)).toBe('15k+');
expect(formatCompactNumber(999999)).toBe('999k+');
});
it('should return millions/billion/trillion format for values 1,000,000 and above', () => {
const { formatCompactNumber } = useNumberFormatter();
expect(formatCompactNumber(1000000)).toBe('1M');
expect(formatCompactNumber(1000001)).toBe('1.0M');
expect(formatCompactNumber(1200000)).toBe('1.2M');
expect(formatCompactNumber(1234000)).toBe('1.2M');
expect(formatCompactNumber(2500000)).toBe('2.5M');
expect(formatCompactNumber(10000000)).toBe('10M');
expect(formatCompactNumber(1000000000)).toBe('1B');
expect(formatCompactNumber(1100000000)).toBe('1.1B');
expect(formatCompactNumber(10000000000)).toBe('10B');
expect(formatCompactNumber(11000000000)).toBe('11B');
expect(formatCompactNumber(1000000000000)).toBe('1T');
expect(formatCompactNumber(1100000000000)).toBe('1.1T');
expect(formatCompactNumber(10000000000000)).toBe('10T');
expect(formatCompactNumber(11000000000000)).toBe('11T');
});
it('should handle edge cases gracefully', () => {
const { formatCompactNumber } = useNumberFormatter();
expect(formatCompactNumber(null)).toBe('0');
expect(formatCompactNumber(undefined)).toBe('0');
expect(formatCompactNumber(NaN)).toBe('0');
expect(formatCompactNumber('string')).toBe('0');
});
it('should handle negative numbers', () => {
const { formatCompactNumber } = useNumberFormatter();
expect(formatCompactNumber(-500)).toBe('-500');
expect(formatCompactNumber(-1000)).toBe('-1k');
expect(formatCompactNumber(-1500)).toBe('-1k+');
expect(formatCompactNumber(-2000)).toBe('-2k');
expect(formatCompactNumber(-1200000)).toBe('-1.2M');
});
it('should format with en-US locale', () => {
const mockLocale = ref('en-US');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatCompactNumber } = useNumberFormatter();
expect(formatCompactNumber(1500)).toBe('1k+');
});
it('should format with de-DE locale', () => {
const mockLocale = ref('de-DE');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatCompactNumber } = useNumberFormatter();
expect(formatCompactNumber(2000)).toBe('2k');
});
it('should format with fr-FR locale (compact notation)', () => {
const mockLocale = ref('fr-FR');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatCompactNumber } = useNumberFormatter();
const result = formatCompactNumber(1000000);
expect(result).toMatch(/1\s*M/); // French uses space before M
});
it('should format with ja-JP locale', () => {
const mockLocale = ref('ja-JP');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatCompactNumber } = useNumberFormatter();
expect(formatCompactNumber(999)).toBe('999');
});
it('should format with ar-SA locale (Arabic numerals)', () => {
const mockLocale = ref('ar-SA');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatCompactNumber } = useNumberFormatter();
const result = formatCompactNumber(5000);
expect(result).toContain('k');
expect(typeof result).toBe('string');
});
it('should format with es-ES locale', () => {
const mockLocale = ref('es-ES');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatCompactNumber } = useNumberFormatter();
expect(formatCompactNumber(7500)).toBe('7k+');
});
it('should format with hi-IN locale', () => {
const mockLocale = ref('hi-IN');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatCompactNumber } = useNumberFormatter();
expect(formatCompactNumber(100000)).toBe('100k');
});
it('should format with ru-RU locale', () => {
const mockLocale = ref('ru-RU');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatCompactNumber } = useNumberFormatter();
expect(formatCompactNumber(3000)).toBe('3k');
});
it('should format with ko-KR locale (uses 만 for 10,000)', () => {
const mockLocale = ref('ko-KR');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatCompactNumber } = useNumberFormatter();
const result = formatCompactNumber(2500000);
expect(result).toContain('만'); // Korean uses 만 (10,000) as a unit, so 2,500,000 should contain 만
});
it('should format with pt-BR locale', () => {
const mockLocale = ref('pt-BR');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatCompactNumber } = useNumberFormatter();
expect(formatCompactNumber(8888)).toBe('8k+');
});
it('should handle underscore-based locale tags (pt_BR)', () => {
const mockLocale = ref('pt_BR');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatCompactNumber } = useNumberFormatter();
// Should normalize pt_BR to pt-BR and work correctly
expect(formatCompactNumber(8888)).toBe('8k+');
expect(formatCompactNumber(1000000)).toBe('1\u00a0mi');
});
it('should handle underscore-based locale tags (zh_CN)', () => {
const mockLocale = ref('zh_CN');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatCompactNumber } = useNumberFormatter();
// Should normalize zh_CN to zh-CN and work correctly
expect(formatCompactNumber(999)).toBe('999');
expect(formatCompactNumber(5000)).toBe('5k');
});
it('should handle underscore-based locale tags (en_US)', () => {
const mockLocale = ref('en_US');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatCompactNumber } = useNumberFormatter();
// Should normalize en_US to en-US and work correctly
expect(formatCompactNumber(1500)).toBe('1k+');
expect(formatCompactNumber(1000000)).toBe('1M');
});
it('should handle null/undefined locale gracefully', () => {
const mockLocale = ref(null);
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatCompactNumber } = useNumberFormatter();
// Should fall back to 'en' locale
expect(formatCompactNumber(1500)).toBe('1k+');
});
it('should fall back to base language when specific locale not supported', () => {
// Simulate a case where pt-BR might not be fully supported but pt is
const mockLocale = ref('pt-BR');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatCompactNumber } = useNumberFormatter();
// Should work with either pt-BR or pt fallback
const result = formatCompactNumber(1500);
expect(result).toMatch(/1k\+/);
});
it('should fall back to English for completely unsupported locales', () => {
// Use a completely made-up locale
const mockLocale = ref('xx-YY');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatCompactNumber } = useNumberFormatter();
// Should fall back to 'en' and work
expect(formatCompactNumber(1500)).toBe('1k+');
expect(formatCompactNumber(1000000)).toBe('1M');
});
it('should handle edge case with only base language code', () => {
const mockLocale = ref('pt');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatCompactNumber } = useNumberFormatter();
// Should work with base language
expect(formatCompactNumber(2000)).toBe('2k');
});
});
describe('formatFullNumber', () => {
it('should format numbers with locale-specific formatting', () => {
const { formatFullNumber } = useNumberFormatter();
expect(formatFullNumber(1000)).toBe('1,000');
expect(formatFullNumber(1234567)).toBe('1,234,567');
expect(formatFullNumber(1234567890)).toBe('1,234,567,890');
expect(formatFullNumber(1234567890123)).toBe('1,234,567,890,123');
});
it('should handle edge cases gracefully', () => {
const { formatFullNumber } = useNumberFormatter();
expect(formatFullNumber(null)).toBe('0');
expect(formatFullNumber(undefined)).toBe('0');
expect(formatFullNumber(NaN)).toBe('0');
expect(formatFullNumber('string')).toBe('0');
});
it('should format with en-US locale (comma separator)', () => {
const mockLocale = ref('en-US');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatFullNumber } = useNumberFormatter();
expect(formatFullNumber(1234567)).toBe('1,234,567');
});
it('should format with de-DE locale (period separator)', () => {
const mockLocale = ref('de-DE');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatFullNumber } = useNumberFormatter();
expect(formatFullNumber(9876543)).toBe('9.876.543');
});
it('should format with es-ES locale (period separator)', () => {
const mockLocale = ref('es-ES');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatFullNumber } = useNumberFormatter();
expect(formatFullNumber(5555555)).toBe('5.555.555');
});
it('should format with zh-CN locale', () => {
const mockLocale = ref('zh-CN');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatFullNumber } = useNumberFormatter();
expect(formatFullNumber(1000000)).toBe('1,000,000');
});
it('should format with ar-EG locale (Arabic numerals, RTL)', () => {
const mockLocale = ref('ar-EG');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatFullNumber } = useNumberFormatter();
const result = formatFullNumber(7654321);
// Arabic locale uses Eastern Arabic numerals (٠-٩)
// Just verify it's formatted (length should be reasonable)
expect(result.length).toBeGreaterThan(6);
});
it('should format with fr-FR locale (narrow no-break space)', () => {
const mockLocale = ref('fr-FR');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatFullNumber } = useNumberFormatter();
const result = formatFullNumber(3333333);
expect(result).toContain('3');
expect(result).toContain('333');
});
it('should format with hi-IN locale (Indian numbering system)', () => {
const mockLocale = ref('hi-IN');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatFullNumber } = useNumberFormatter();
expect(formatFullNumber(9999999)).toBe('99,99,999');
});
it('should format with th-TH locale', () => {
const mockLocale = ref('th-TH');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatFullNumber } = useNumberFormatter();
expect(formatFullNumber(4444444)).toBe('4,444,444');
});
it('should format with tr-TR locale', () => {
const mockLocale = ref('tr-TR');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatFullNumber } = useNumberFormatter();
expect(formatFullNumber(6666666)).toBe('6.666.666');
});
it('should format with pt-PT locale (space separator)', () => {
const mockLocale = ref('pt-PT');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatFullNumber } = useNumberFormatter();
const result = formatFullNumber(2222222);
// Portuguese (Portugal) uses narrow no-break space as separator
expect(result).toMatch(/2[\s\u202f]222[\s\u202f]222/);
});
it('should handle underscore-based locale tags (pt_BR)', () => {
const mockLocale = ref('pt_BR');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatFullNumber } = useNumberFormatter();
// Should normalize pt_BR to pt-BR and work correctly
expect(formatFullNumber(1234567)).toBe('1.234.567');
});
it('should handle underscore-based locale tags (zh_CN)', () => {
const mockLocale = ref('zh_CN');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatFullNumber } = useNumberFormatter();
// Should normalize zh_CN to zh-CN and work correctly
expect(formatFullNumber(1000000)).toBe('1,000,000');
});
it('should handle underscore-based locale tags (en_US)', () => {
const mockLocale = ref('en_US');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatFullNumber } = useNumberFormatter();
// Should normalize en_US to en-US and work correctly
expect(formatFullNumber(1234567)).toBe('1,234,567');
});
it('should handle null/undefined locale gracefully', () => {
const mockLocale = ref(null);
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatFullNumber } = useNumberFormatter();
// Should fall back to 'en' locale
expect(formatFullNumber(1234567)).toBe('1,234,567');
});
it('should fall back to base language when specific locale not supported', () => {
// Simulate a case where pt-BR might not be fully supported but pt is
const mockLocale = ref('pt-BR');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatFullNumber } = useNumberFormatter();
// Should work with either pt-BR or pt fallback
const result = formatFullNumber(1234567);
// Portuguese uses period as thousands separator
expect(result).toMatch(/1[.,\s]234[.,\s]567/);
});
it('should fall back to English for completely unsupported locales', () => {
// Use a completely made-up locale
const mockLocale = ref('xx-YY');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatFullNumber } = useNumberFormatter();
// Should fall back to 'en' and work
expect(formatFullNumber(1234567)).toBe('1,234,567');
});
it('should handle edge case with only base language code', () => {
const mockLocale = ref('pt');
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
const { formatFullNumber } = useNumberFormatter();
// Should work with base language
const result = formatFullNumber(1000000);
expect(result).toMatch(/1[.,\s]000[.,\s]000/);
});
});
});