From e68bf0cf4e77f149d9377d47a67f93e163874146 Mon Sep 17 00:00:00 2001 From: Imran Jamil Malik Date: Tue, 26 May 2026 13:02:34 +0500 Subject: [PATCH 1/3] Fix: Allow colon symbol : as a valid selectable category --- src/libs/CategoryUtils.ts | 6 ++++++ 1 file changed, 6 insertions(+) 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) { From 4be7ec14084b3455c5fb4305fabf7b8afcfa94dc Mon Sep 17 00:00:00 2001 From: Imran Jamil Malik Date: Wed, 27 May 2026 17:19:44 +0500 Subject: [PATCH 2/3] =?UTF-8?q?test:=20add=20unit=20tests=20for=20colon?= =?UTF-8?q?=E2=80=91only=20category=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add tests for processCategoryNameSegments and getDecodedLeafCategoryName in CategoryUtilsTest - Add test for sortCategories handling of colon‑only categories in CategoryOptionListUtilsTest - Verify that ':' and '::' are treated as valid leaf categories --- tests/unit/CategoryOptionListUtilsTest.ts | 74 +++++++++++++++++++++++ tests/unit/CategoryUtilsTest.ts | 29 ++++++++- 2 files changed, 102 insertions(+), 1 deletion(-) 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..d17a7e32e932 100644 --- a/tests/unit/CategoryUtilsTest.ts +++ b/tests/unit/CategoryUtilsTest.ts @@ -1,5 +1,5 @@ import type {OnyxCollection} from 'react-native-onyx'; -import {formatRequireItemizedReceiptsOverText, getAvailableNonPersonalPolicyCategories, isCategoryDescriptionRequired, isCategoryMissing} from '@libs/CategoryUtils'; +import {formatRequireItemizedReceiptsOverText, getAvailableNonPersonalPolicyCategories, isCategoryDescriptionRequired, isCategoryMissing, processCategoryNameSegments, getDecodedLeafCategoryName} from '@libs/CategoryUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -207,4 +207,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:'); + }); + }); + }); }); From eac8ba0bc1d386cdfafb817538f5d420764c2e5f Mon Sep 17 00:00:00 2001 From: Imran Jamil Malik Date: Wed, 27 May 2026 17:39:52 +0500 Subject: [PATCH 3/3] Prettier --- tests/unit/CategoryUtilsTest.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/unit/CategoryUtilsTest.ts b/tests/unit/CategoryUtilsTest.ts index d17a7e32e932..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, processCategoryNameSegments, getDecodedLeafCategoryName} 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';