diff --git a/src/libs/CategoryUtils.ts b/src/libs/CategoryUtils.ts index fcf329257499..3024bb144f63 100644 --- a/src/libs/CategoryUtils.ts +++ b/src/libs/CategoryUtils.ts @@ -175,6 +175,12 @@ function processCategoryNameSegments(categoryName: string): string[] { } } + // If all segments were empty but the original name is not empty, + // treat the whole name as a single segment (e.g., ":" or "::"). + if (result.length === 0 && categoryName.trim() !== '') { + return [categoryName.trim()]; + } + // If the original name ends with a colon (allowing trailing spaces), append a colon to the last segment. const endsWithColon = categoryName.trim().endsWith(CONST.PARENT_CHILD_SEPARATOR); if (endsWithColon && result.length > 0) { diff --git a/tests/unit/CategoryOptionListUtilsTest.ts b/tests/unit/CategoryOptionListUtilsTest.ts index ada043b7510c..c1d8c2812e24 100644 --- a/tests/unit/CategoryOptionListUtilsTest.ts +++ b/tests/unit/CategoryOptionListUtilsTest.ts @@ -851,6 +851,66 @@ describe('CategoryOptionListUtils', () => { expect(getCategoryOptionTree(categories)).toStrictEqual(result); }); + it('handles colon‑only category names', () => { + const categories = { + ':': { + enabled: true, + name: ':', + }, + '::': { + enabled: true, + name: '::', + }, + ' : ': { + enabled: true, + name: ' : ', + }, + 'Normal:Category': { + enabled: true, + name: 'Normal:Category', + }, + }; + + const result = getCategoryOptionTree(categories); + + // The colon-only categories should appear as top‑level leaf items (no indentation) + // They should have the exact name as both text and searchText. + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + text: ':', + keyForList: ':', + searchText: ':', + isDisabled: false, + }), + expect.objectContaining({ + text: '::', + keyForList: '::', + searchText: '::', + isDisabled: false, + }), + expect.objectContaining({ + text: ':', + keyForList: ' : ', + searchText: ' : ', + isDisabled: false, + }), + expect.objectContaining({ + text: 'Normal', + keyForList: 'Normal', + searchText: 'Normal', + isDisabled: true, + }), + expect.objectContaining({ + text: ' Category', + keyForList: 'Normal:Category', + searchText: 'Normal:Category', + isDisabled: false, + }), + ]), + ); + }); + it('sortCategories', () => { const categoriesIncorrectOrdering = { Taxi: { @@ -1203,4 +1263,18 @@ describe('CategoryOptionListUtils', () => { expect(sortCategories(categoriesIncorrectOrdering2, localeCompare)).toStrictEqual(result2); expect(sortCategories(categoriesIncorrectOrdering3, localeCompare)).toStrictEqual(result3); }); + + it('sortCategories keeps colon‑only categories', () => { + const categories = { + ':': {enabled: true, name: ':'}, + '::': {enabled: true, name: '::'}, + 'Normal:Category': {enabled: true, name: 'Normal:Category'}, + }; + const sorted = sortCategories(categories, localeCompare); + expect(sorted).toEqual([ + {name: ':', enabled: true, pendingAction: undefined}, + {name: '::', enabled: true, pendingAction: undefined}, + {name: 'Normal:Category', enabled: true, pendingAction: undefined}, + ]); + }); }); diff --git a/tests/unit/CategoryUtilsTest.ts b/tests/unit/CategoryUtilsTest.ts index 57a9bcae9db7..3c2439e49fc7 100644 --- a/tests/unit/CategoryUtilsTest.ts +++ b/tests/unit/CategoryUtilsTest.ts @@ -1,5 +1,12 @@ import type {OnyxCollection} from 'react-native-onyx'; -import {formatRequireItemizedReceiptsOverText, getAvailableNonPersonalPolicyCategories, isCategoryDescriptionRequired, isCategoryMissing} from '@libs/CategoryUtils'; +import { + formatRequireItemizedReceiptsOverText, + getAvailableNonPersonalPolicyCategories, + getDecodedLeafCategoryName, + isCategoryDescriptionRequired, + isCategoryMissing, + processCategoryNameSegments, +} from '@libs/CategoryUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -207,4 +214,31 @@ describe('getAvailableNonPersonalPolicyCategories', () => { expect(result[keyOther]?.TestCategory2).toBeDefined(); expect(result[keyOther]?.TestCategory3).toBeDefined(); }); + + describe('processCategoryNameSegments and getDecodedLeafCategoryName', () => { + describe('processCategoryNameSegments', () => { + it('returns a single segment for colon‑only names', () => { + expect(processCategoryNameSegments(':')).toEqual([':']); + expect(processCategoryNameSegments('::')).toEqual(['::']); + }); + + it('handles normal hierarchical categories unchanged (preserves leading spaces)', () => { + expect(processCategoryNameSegments('Food: Meat')).toEqual(['Food', ' Meat']); + expect(processCategoryNameSegments('A: B:')).toEqual(['A', ' B:']); + expect(processCategoryNameSegments('Parent:Child')).toEqual(['Parent', 'Child']); + }); + }); + + describe('getDecodedLeafCategoryName', () => { + it('returns the leaf name for colon‑only categories', () => { + expect(getDecodedLeafCategoryName(':')).toEqual(':'); + expect(getDecodedLeafCategoryName('::')).toEqual('::'); + }); + + it('returns the leaf for normal hierarchies (trimmed)', () => { + expect(getDecodedLeafCategoryName('Food: Meat')).toEqual('Meat'); + expect(getDecodedLeafCategoryName('A: B:')).toEqual('B:'); + }); + }); + }); });