From 746e2a7e55d8ab79ef6b0d2fd96192ad1f363c6b Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Tue, 10 Jun 2025 13:26:56 -0700 Subject: [PATCH 1/6] fix(validation): update merchant validation length check to bytes --- src/CONST.ts | 2 +- .../SearchFiltersMerchantPage.tsx | 6 ++++-- src/pages/iou/request/step/IOURequestStepMerchant.tsx | 8 +++++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index f6aefe3cf254..f600a8e0a50f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -526,7 +526,7 @@ const CONST = { // Prevents consecutive special characters or spaces like '--', '..', '((', '))', or ' '. REPEATED_SPECIAL_CHAR_PATTERN: /([-\s().])\1+/, - MERCHANT_NAME_MAX_LENGTH: 255, + MERCHANT_NAME_MAX_BYTES: 255, MASKED_PAN_PREFIX: 'XXXXXXXXXXXX', diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersMerchantPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersMerchantPage.tsx index 4a519d94aefc..22add2ba54af 100644 --- a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersMerchantPage.tsx +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersMerchantPage.tsx @@ -16,6 +16,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import FILTER_KEYS from '@src/types/form/SearchAdvancedFiltersForm'; +import {Buffer} from 'buffer/'; function SearchFiltersMerchantPage() { const styles = useThemeStyles(); @@ -33,9 +34,10 @@ function SearchFiltersMerchantPage() { const validate = (values: FormOnyxValues) => { const errors: FormInputErrors = {}; const merchantValue = values.merchant.trim(); + const merchantByteLength = Buffer.from(merchantValue).length; - if (merchantValue.length > CONST.MERCHANT_NAME_MAX_LENGTH) { - errors.merchant = translate('common.error.characterLimitExceedCounter', {length: merchantValue.length, limit: CONST.MERCHANT_NAME_MAX_LENGTH}); + if (merchantByteLength > CONST.MERCHANT_NAME_MAX_BYTES) { + errors.merchant = translate('common.error.characterLimitExceedCounter', {length: merchantByteLength, limit: CONST.MERCHANT_NAME_MAX_BYTES}); } return errors; diff --git a/src/pages/iou/request/step/IOURequestStepMerchant.tsx b/src/pages/iou/request/step/IOURequestStepMerchant.tsx index 0d2980ac5fdc..e39a8d7fce3c 100644 --- a/src/pages/iou/request/step/IOURequestStepMerchant.tsx +++ b/src/pages/iou/request/step/IOURequestStepMerchant.tsx @@ -17,6 +17,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/MoneyRequestMerchantForm'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import {Buffer} from 'buffer/'; import DiscardChangesConfirmation from './DiscardChangesConfirmation'; import StepScreenWrapper from './StepScreenWrapper'; import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound'; @@ -60,15 +61,16 @@ function IOURequestStepMerchant({ const validate = useCallback( (value: FormOnyxValues) => { const errors: FormInputErrors = {}; + const merchantByteLength = Buffer.from(value.moneyRequestMerchant).length; if (isMerchantRequired && !value.moneyRequestMerchant) { errors.moneyRequestMerchant = translate('common.error.fieldRequired'); } else if (isMerchantRequired && value.moneyRequestMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT) { errors.moneyRequestMerchant = translate('iou.error.invalidMerchant'); - } else if (value.moneyRequestMerchant.length > CONST.MERCHANT_NAME_MAX_LENGTH) { + } else if (merchantByteLength > CONST.MERCHANT_NAME_MAX_BYTES) { errors.moneyRequestMerchant = translate('common.error.characterLimitExceedCounter', { - length: value.moneyRequestMerchant.length, - limit: CONST.MERCHANT_NAME_MAX_LENGTH, + length: merchantByteLength, + limit: CONST.MERCHANT_NAME_MAX_BYTES, }); } From e59ce566d1658c0e37c10255433456b5e1640bd6 Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Thu, 12 Jun 2025 14:05:53 -0700 Subject: [PATCH 2/6] update(refactor): added validation function and unit tests --- src/libs/ValidationUtils.ts | 23 +++++ .../SearchFiltersMerchantPage.tsx | 8 +- .../request/step/IOURequestStepMerchant.tsx | 8 +- tests/unit/ValidationUtilsTest.ts | 96 +++++++++++++++++++ 4 files changed, 127 insertions(+), 8 deletions(-) diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index a550a8f2f049..83adba0e310d 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -1,3 +1,4 @@ +import {Buffer} from 'buffer'; import {addYears, endOfMonth, format, isAfter, isBefore, isSameDay, isValid, isWithinInterval, parse, parseISO, startOfDay, subYears} from 'date-fns'; import {PUBLIC_DOMAINS_SET, Str, Url} from 'expensify-common'; import isEmpty from 'lodash/isEmpty'; @@ -657,6 +658,27 @@ function isValidRegistrationNumber(registrationNumber: string, country: Country } } +/** + * Checks if the character length of an input string exceeds the specified length, + * returning `isValid` (boolean) and `byteLength` (number) to be used in dynamic error copy. + * + * @remarks + * This function uses `Buffer.from(inputValue).length` to calculate the byte size of the input string in UTF-8 encoding. + * Unlike JavaScript's `string.length` which counts UTF-16 code units (where some characters, like emojis or certain non-Latin characters, may use two code units), + * `Buffer` measures the actual byte size in UTF-8. This is important for non-Latin alphabets (e.g., Sanskrit, Chinese, or emojis) where characters often require more than one byte. + * + * For example: + * - Latin characters (e.g., 'a', 'b') use 1 byte each in UTF-8. + * - Sanskrit characters (e.g., 'क', 'ष') typically use 3 bytes each in UTF-8. + * - Emojis (e.g., '😊') may use 4 bytes in UTF-8. + * + * This distinction is critical when validating input for systems with byte-based size constraints, such as database fields or network protocols. + */ +function isValidInputLength(inputValue: string, length: number) { + const inputValueLength = Buffer.from(inputValue).length; + return {isValid: inputValueLength > length, byteLength: inputValueLength}; +} + export { meetsMinimumAgeRequirement, meetsMaximumAgeRequirement, @@ -708,4 +730,5 @@ export { isValidZipCodeInternational, isValidOwnershipPercentage, isValidRegistrationNumber, + isValidInputLength, }; diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersMerchantPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersMerchantPage.tsx index 22add2ba54af..97817e755c6a 100644 --- a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersMerchantPage.tsx +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersMerchantPage.tsx @@ -12,11 +12,11 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import {updateAdvancedFilters} from '@libs/actions/Search'; import Navigation from '@libs/Navigation/Navigation'; +import {isValidInputLength} from '@libs/ValidationUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import FILTER_KEYS from '@src/types/form/SearchAdvancedFiltersForm'; -import {Buffer} from 'buffer/'; function SearchFiltersMerchantPage() { const styles = useThemeStyles(); @@ -34,10 +34,10 @@ function SearchFiltersMerchantPage() { const validate = (values: FormOnyxValues) => { const errors: FormInputErrors = {}; const merchantValue = values.merchant.trim(); - const merchantByteLength = Buffer.from(merchantValue).length; + const {isValid, byteLength} = isValidInputLength(merchantValue, CONST.MERCHANT_NAME_MAX_BYTES); - if (merchantByteLength > CONST.MERCHANT_NAME_MAX_BYTES) { - errors.merchant = translate('common.error.characterLimitExceedCounter', {length: merchantByteLength, limit: CONST.MERCHANT_NAME_MAX_BYTES}); + if (isValid) { + errors.merchant = translate('common.error.characterLimitExceedCounter', {length: byteLength, limit: CONST.MERCHANT_NAME_MAX_BYTES}); } return errors; diff --git a/src/pages/iou/request/step/IOURequestStepMerchant.tsx b/src/pages/iou/request/step/IOURequestStepMerchant.tsx index e39a8d7fce3c..abfb5d77e52d 100644 --- a/src/pages/iou/request/step/IOURequestStepMerchant.tsx +++ b/src/pages/iou/request/step/IOURequestStepMerchant.tsx @@ -11,13 +11,13 @@ import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import {getTransactionDetails, isExpenseRequest, isPolicyExpenseChat} from '@libs/ReportUtils'; +import {isValidInputLength} from '@libs/ValidationUtils'; import {setDraftSplitTransaction, setMoneyRequestMerchant, updateMoneyRequestMerchant} from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/MoneyRequestMerchantForm'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import {Buffer} from 'buffer/'; import DiscardChangesConfirmation from './DiscardChangesConfirmation'; import StepScreenWrapper from './StepScreenWrapper'; import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound'; @@ -61,15 +61,15 @@ function IOURequestStepMerchant({ const validate = useCallback( (value: FormOnyxValues) => { const errors: FormInputErrors = {}; - const merchantByteLength = Buffer.from(value.moneyRequestMerchant).length; + const {isValid, byteLength} = isValidInputLength(value.moneyRequestMerchant, CONST.MERCHANT_NAME_MAX_BYTES); if (isMerchantRequired && !value.moneyRequestMerchant) { errors.moneyRequestMerchant = translate('common.error.fieldRequired'); } else if (isMerchantRequired && value.moneyRequestMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT) { errors.moneyRequestMerchant = translate('iou.error.invalidMerchant'); - } else if (merchantByteLength > CONST.MERCHANT_NAME_MAX_BYTES) { + } else if (isValid) { errors.moneyRequestMerchant = translate('common.error.characterLimitExceedCounter', { - length: merchantByteLength, + length: byteLength, limit: CONST.MERCHANT_NAME_MAX_BYTES, }); } diff --git a/tests/unit/ValidationUtilsTest.ts b/tests/unit/ValidationUtilsTest.ts index 343e05ef2e52..3cf8897cef43 100644 --- a/tests/unit/ValidationUtilsTest.ts +++ b/tests/unit/ValidationUtilsTest.ts @@ -7,6 +7,7 @@ import { isValidAccountRoute, isValidDate, isValidExpirationDate, + isValidInputLength, isValidLegalName, isValidPastDate, isValidPaymentZipCode, @@ -396,4 +397,99 @@ describe('ValidationUtils', () => { expect(isValid).toBe(false); }); }); + + describe('isValidInputLength', () => { + // Test Latin alphabet characters (1 byte each in UTF-8) + describe('Latin alphabet characters', () => { + test('returns true and correct byte length when Latin string byte length exceeds limit', () => { + expect(isValidInputLength('abc', 2)).toEqual({isValid: true, byteLength: 3}); // 3 bytes > 2 + }); + + test('returns false and correct byte length when Latin string byte length equals limit', () => { + expect(isValidInputLength('abc', 3)).toEqual({isValid: false, byteLength: 3}); // 3 bytes ≤ 3 + }); + + test('returns false and correct byte length when Latin string byte length is less than limit', () => { + expect(isValidInputLength('ab', 3)).toEqual({isValid: false, byteLength: 2}); // 2 bytes ≤ 3 + }); + }); + + // Test Sanskrit characters (typically 3 bytes each in UTF-8) + describe('Sanskrit characters', () => { + test('returns true and correct byte length when Sanskrit string byte length exceeds limit', () => { + expect(isValidInputLength('कष', 5)).toEqual({isValid: true, byteLength: 6}); // 6 bytes > 5 + }); + + test('returns false and correct byte length when Sanskrit string byte length equals limit', () => { + expect(isValidInputLength('कष', 6)).toEqual({isValid: false, byteLength: 6}); // 6 bytes ≤ 6 + }); + + test('returns false and correct byte length when Sanskrit string byte length is less than limit', () => { + expect(isValidInputLength('क', 4)).toEqual({isValid: false, byteLength: 3}); // 3 bytes ≤ 4 + }); + }); + + // Test emojis (typically 4 bytes each in UTF-8) + describe('Emojis', () => { + test('returns true and correct byte length when emoji byte length exceeds limit', () => { + expect(isValidInputLength('😊', 3)).toEqual({isValid: true, byteLength: 4}); // 4 bytes > 3 + }); + + test('returns false and correct byte length when emoji byte length equals limit', () => { + expect(isValidInputLength('😊', 4)).toEqual({isValid: false, byteLength: 4}); // 4 bytes ≤ 4 + }); + + test('returns false and correct byte length when emoji byte length is less than limit', () => { + expect(isValidInputLength('😊', 5)).toEqual({isValid: false, byteLength: 4}); // 4 bytes ≤ 5 + }); + }); + + // Test empty strings and spaces + describe('Empty strings and spaces', () => { + test('returns false and correct byte length for empty string regardless of limit', () => { + expect(isValidInputLength('', 0)).toEqual({isValid: false, byteLength: 0}); // 0 bytes ≤ 0 + expect(isValidInputLength('', 1)).toEqual({isValid: false, byteLength: 0}); // 0 bytes ≤ 1 + }); + + test('returns true and correct byte length when space string byte length exceeds limit', () => { + expect(isValidInputLength(' ', 2)).toEqual({isValid: true, byteLength: 3}); // 3 bytes > 2 + }); + + test('returns false and correct byte length when space string byte length equals limit', () => { + expect(isValidInputLength(' ', 2)).toEqual({isValid: false, byteLength: 2}); // 2 bytes ≤ 2 + }); + }); + + // Test mixed characters + describe('Mixed characters', () => { + test('returns true and correct byte length when mixed string byte length exceeds limit', () => { + expect(isValidInputLength('aक😊', 6)).toEqual({isValid: true, byteLength: 8}); // 1 + 3 + 4 = 8 bytes > 6 + }); + + test('returns false and correct byte length when mixed string byte length equals limit', () => { + expect(isValidInputLength('aक😊', 8)).toEqual({isValid: false, byteLength: 8}); // 1 + 3 + 4 = 8 bytes ≤ 8 + }); + + test('returns false and correct byte length when mixed string byte length is less than limit', () => { + expect(isValidInputLength('aक', 5)).toEqual({isValid: false, byteLength: 4}); // 1 + 3 = 4 bytes ≤ 5 + }); + }); + + // Test edge cases + describe('Edge cases', () => { + test('handles negative length parameter', () => { + expect(isValidInputLength('abc', -1)).toEqual({isValid: true, byteLength: 3}); // 3 bytes > -1 + }); + + test('handles zero length parameter', () => { + expect(isValidInputLength('a', 0)).toEqual({isValid: true, byteLength: 1}); // 1 byte > 0 + expect(isValidInputLength('', 0)).toEqual({isValid: false, byteLength: 0}); // 0 bytes ≤ 0 + }); + + test('handles special characters (e.g., newlines, tabs)', () => { + expect(isValidInputLength('\n\t', 1)).toEqual({isValid: true, byteLength: 2}); // 2 bytes > 1 + expect(isValidInputLength('\n\t', 2)).toEqual({isValid: false, byteLength: 2}); // 2 bytes ≤ 2 + }); + }); + }); }); From 624a2b3366eb2a9087ca82a4cb5070dda3913fb0 Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Thu, 12 Jun 2025 14:51:12 -0700 Subject: [PATCH 3/6] fix(eslint): added canBeMissing and code review adjustments --- src/libs/ValidationUtils.ts | 22 +++++-------------- .../SearchFiltersMerchantPage.tsx | 2 +- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 83adba0e310d..8c3cccf52422 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -659,24 +659,12 @@ function isValidRegistrationNumber(registrationNumber: string, country: Country } /** - * Checks if the character length of an input string exceeds the specified length, + * Checks if the `inputValue` byte length exceeds the specified byte length, * returning `isValid` (boolean) and `byteLength` (number) to be used in dynamic error copy. - * - * @remarks - * This function uses `Buffer.from(inputValue).length` to calculate the byte size of the input string in UTF-8 encoding. - * Unlike JavaScript's `string.length` which counts UTF-16 code units (where some characters, like emojis or certain non-Latin characters, may use two code units), - * `Buffer` measures the actual byte size in UTF-8. This is important for non-Latin alphabets (e.g., Sanskrit, Chinese, or emojis) where characters often require more than one byte. - * - * For example: - * - Latin characters (e.g., 'a', 'b') use 1 byte each in UTF-8. - * - Sanskrit characters (e.g., 'क', 'ष') typically use 3 bytes each in UTF-8. - * - Emojis (e.g., '😊') may use 4 bytes in UTF-8. - * - * This distinction is critical when validating input for systems with byte-based size constraints, such as database fields or network protocols. - */ -function isValidInputLength(inputValue: string, length: number) { - const inputValueLength = Buffer.from(inputValue).length; - return {isValid: inputValueLength > length, byteLength: inputValueLength}; + */ +function isValidInputLength(inputValue: string, byteLength: number) { + const valueByteLength = Buffer.from(inputValue).length; + return {isValid: valueByteLength > byteLength, byteLength: valueByteLength}; } export { diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersMerchantPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersMerchantPage.tsx index 97817e755c6a..6368b6854a03 100644 --- a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersMerchantPage.tsx +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersMerchantPage.tsx @@ -22,7 +22,7 @@ function SearchFiltersMerchantPage() { const styles = useThemeStyles(); const {translate} = useLocalize(); - const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM); + const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {canBeMissing: true}); const merchant = searchAdvancedFiltersForm?.[FILTER_KEYS.MERCHANT]; const {inputCallbackRef} = useAutoFocusInput(); From 995758a756e92133923f866f3d9a37de27377d77 Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Sat, 14 Jun 2025 16:54:01 -0700 Subject: [PATCH 4/6] update(api): switched to TextEncoder API --- src/libs/StringUtils/index.ts | 10 ++++++++++ src/libs/ValidationUtils.ts | 3 +-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/libs/StringUtils/index.ts b/src/libs/StringUtils/index.ts index 6d4f1c0ba1c6..f69163752402 100644 --- a/src/libs/StringUtils/index.ts +++ b/src/libs/StringUtils/index.ts @@ -139,6 +139,15 @@ function removePreCodeBlock(text = '') { return text.replace(/]*>|<\/pre>/g, ''); } +/** + * Returns the number of bytes required to encode a string in UTF-8. + */ +function getUTF8ByteLength(str: string) { + const encoder = new TextEncoder(); + const bytes = encoder.encode(str); + return bytes.length; +} + export default { sanitizeString, isEmptyString, @@ -153,4 +162,5 @@ export default { sortStringArrayByLength, dedent, hash, + getUTF8ByteLength, }; diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 8c3cccf52422..8cbbb22cd536 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -1,4 +1,3 @@ -import {Buffer} from 'buffer'; import {addYears, endOfMonth, format, isAfter, isBefore, isSameDay, isValid, isWithinInterval, parse, parseISO, startOfDay, subYears} from 'date-fns'; import {PUBLIC_DOMAINS_SET, Str, Url} from 'expensify-common'; import isEmpty from 'lodash/isEmpty'; @@ -663,7 +662,7 @@ function isValidRegistrationNumber(registrationNumber: string, country: Country * returning `isValid` (boolean) and `byteLength` (number) to be used in dynamic error copy. */ function isValidInputLength(inputValue: string, byteLength: number) { - const valueByteLength = Buffer.from(inputValue).length; + const valueByteLength = StringUtils.getUTF8ByteLength(inputValue); return {isValid: valueByteLength > byteLength, byteLength: valueByteLength}; } From 88ddfa9c92e222ee69022495ac911a48e55a7021 Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Sat, 14 Jun 2025 17:42:20 -0700 Subject: [PATCH 5/6] fix(tests): added TextEncoder global mock --- tests/unit/ValidationUtilsTest.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/ValidationUtilsTest.ts b/tests/unit/ValidationUtilsTest.ts index 3cf8897cef43..09fe2bca58f2 100644 --- a/tests/unit/ValidationUtilsTest.ts +++ b/tests/unit/ValidationUtilsTest.ts @@ -1,4 +1,5 @@ import {addDays, format, startOfDay, subYears} from 'date-fns'; +import {TextEncoder} from 'util'; import {translateLocal} from '@libs/Localize'; import CONST from '@src/CONST'; import { @@ -19,6 +20,8 @@ import { meetsMinimumAgeRequirement, } from '@src/libs/ValidationUtils'; +global.TextEncoder = TextEncoder; + describe('ValidationUtils', () => { describe('isValidDate', () => { test('Should return true for a valid date within the range', () => { From 5bab1071a9cfc8c11dc81042b46142f3f291daa1 Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Mon, 16 Jun 2025 11:03:09 -0700 Subject: [PATCH 6/6] update(review): adjusted isValidInputLength return condition and tests --- src/libs/ValidationUtils.ts | 2 +- .../SearchFiltersMerchantPage.tsx | 2 +- .../request/step/IOURequestStepMerchant.tsx | 2 +- tests/unit/ValidationUtilsTest.ts | 42 +++++++++---------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 8cbbb22cd536..69570619da2e 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -663,7 +663,7 @@ function isValidRegistrationNumber(registrationNumber: string, country: Country */ function isValidInputLength(inputValue: string, byteLength: number) { const valueByteLength = StringUtils.getUTF8ByteLength(inputValue); - return {isValid: valueByteLength > byteLength, byteLength: valueByteLength}; + return {isValid: valueByteLength <= byteLength, byteLength: valueByteLength}; } export { diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersMerchantPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersMerchantPage.tsx index 6368b6854a03..a1ebf1f45020 100644 --- a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersMerchantPage.tsx +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersMerchantPage.tsx @@ -36,7 +36,7 @@ function SearchFiltersMerchantPage() { const merchantValue = values.merchant.trim(); const {isValid, byteLength} = isValidInputLength(merchantValue, CONST.MERCHANT_NAME_MAX_BYTES); - if (isValid) { + if (!isValid) { errors.merchant = translate('common.error.characterLimitExceedCounter', {length: byteLength, limit: CONST.MERCHANT_NAME_MAX_BYTES}); } diff --git a/src/pages/iou/request/step/IOURequestStepMerchant.tsx b/src/pages/iou/request/step/IOURequestStepMerchant.tsx index abfb5d77e52d..af14bc01e941 100644 --- a/src/pages/iou/request/step/IOURequestStepMerchant.tsx +++ b/src/pages/iou/request/step/IOURequestStepMerchant.tsx @@ -67,7 +67,7 @@ function IOURequestStepMerchant({ errors.moneyRequestMerchant = translate('common.error.fieldRequired'); } else if (isMerchantRequired && value.moneyRequestMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT) { errors.moneyRequestMerchant = translate('iou.error.invalidMerchant'); - } else if (isValid) { + } else if (!isValid) { errors.moneyRequestMerchant = translate('common.error.characterLimitExceedCounter', { length: byteLength, limit: CONST.MERCHANT_NAME_MAX_BYTES, diff --git a/tests/unit/ValidationUtilsTest.ts b/tests/unit/ValidationUtilsTest.ts index 09fe2bca58f2..e9e10dc4f138 100644 --- a/tests/unit/ValidationUtilsTest.ts +++ b/tests/unit/ValidationUtilsTest.ts @@ -405,93 +405,93 @@ describe('ValidationUtils', () => { // Test Latin alphabet characters (1 byte each in UTF-8) describe('Latin alphabet characters', () => { test('returns true and correct byte length when Latin string byte length exceeds limit', () => { - expect(isValidInputLength('abc', 2)).toEqual({isValid: true, byteLength: 3}); // 3 bytes > 2 + expect(isValidInputLength('abc', 2)).toEqual({isValid: false, byteLength: 3}); // 3 bytes > 2 }); test('returns false and correct byte length when Latin string byte length equals limit', () => { - expect(isValidInputLength('abc', 3)).toEqual({isValid: false, byteLength: 3}); // 3 bytes ≤ 3 + expect(isValidInputLength('abc', 3)).toEqual({isValid: true, byteLength: 3}); // 3 bytes ≤ 3 }); test('returns false and correct byte length when Latin string byte length is less than limit', () => { - expect(isValidInputLength('ab', 3)).toEqual({isValid: false, byteLength: 2}); // 2 bytes ≤ 3 + expect(isValidInputLength('ab', 3)).toEqual({isValid: true, byteLength: 2}); // 2 bytes ≤ 3 }); }); // Test Sanskrit characters (typically 3 bytes each in UTF-8) describe('Sanskrit characters', () => { test('returns true and correct byte length when Sanskrit string byte length exceeds limit', () => { - expect(isValidInputLength('कष', 5)).toEqual({isValid: true, byteLength: 6}); // 6 bytes > 5 + expect(isValidInputLength('कष', 5)).toEqual({isValid: false, byteLength: 6}); // 6 bytes > 5 }); test('returns false and correct byte length when Sanskrit string byte length equals limit', () => { - expect(isValidInputLength('कष', 6)).toEqual({isValid: false, byteLength: 6}); // 6 bytes ≤ 6 + expect(isValidInputLength('कष', 6)).toEqual({isValid: true, byteLength: 6}); // 6 bytes ≤ 6 }); test('returns false and correct byte length when Sanskrit string byte length is less than limit', () => { - expect(isValidInputLength('क', 4)).toEqual({isValid: false, byteLength: 3}); // 3 bytes ≤ 4 + expect(isValidInputLength('क', 4)).toEqual({isValid: true, byteLength: 3}); // 3 bytes ≤ 4 }); }); // Test emojis (typically 4 bytes each in UTF-8) describe('Emojis', () => { test('returns true and correct byte length when emoji byte length exceeds limit', () => { - expect(isValidInputLength('😊', 3)).toEqual({isValid: true, byteLength: 4}); // 4 bytes > 3 + expect(isValidInputLength('😊', 3)).toEqual({isValid: false, byteLength: 4}); // 4 bytes > 3 }); test('returns false and correct byte length when emoji byte length equals limit', () => { - expect(isValidInputLength('😊', 4)).toEqual({isValid: false, byteLength: 4}); // 4 bytes ≤ 4 + expect(isValidInputLength('😊', 4)).toEqual({isValid: true, byteLength: 4}); // 4 bytes ≤ 4 }); test('returns false and correct byte length when emoji byte length is less than limit', () => { - expect(isValidInputLength('😊', 5)).toEqual({isValid: false, byteLength: 4}); // 4 bytes ≤ 5 + expect(isValidInputLength('😊', 5)).toEqual({isValid: true, byteLength: 4}); // 4 bytes ≤ 5 }); }); // Test empty strings and spaces describe('Empty strings and spaces', () => { test('returns false and correct byte length for empty string regardless of limit', () => { - expect(isValidInputLength('', 0)).toEqual({isValid: false, byteLength: 0}); // 0 bytes ≤ 0 - expect(isValidInputLength('', 1)).toEqual({isValid: false, byteLength: 0}); // 0 bytes ≤ 1 + expect(isValidInputLength('', 0)).toEqual({isValid: true, byteLength: 0}); // 0 bytes ≤ 0 + expect(isValidInputLength('', 1)).toEqual({isValid: true, byteLength: 0}); // 0 bytes ≤ 1 }); test('returns true and correct byte length when space string byte length exceeds limit', () => { - expect(isValidInputLength(' ', 2)).toEqual({isValid: true, byteLength: 3}); // 3 bytes > 2 + expect(isValidInputLength(' ', 2)).toEqual({isValid: false, byteLength: 3}); // 3 bytes > 2 }); test('returns false and correct byte length when space string byte length equals limit', () => { - expect(isValidInputLength(' ', 2)).toEqual({isValid: false, byteLength: 2}); // 2 bytes ≤ 2 + expect(isValidInputLength(' ', 2)).toEqual({isValid: true, byteLength: 2}); // 2 bytes ≤ 2 }); }); // Test mixed characters describe('Mixed characters', () => { test('returns true and correct byte length when mixed string byte length exceeds limit', () => { - expect(isValidInputLength('aक😊', 6)).toEqual({isValid: true, byteLength: 8}); // 1 + 3 + 4 = 8 bytes > 6 + expect(isValidInputLength('aक😊', 6)).toEqual({isValid: false, byteLength: 8}); // 1 + 3 + 4 = 8 bytes > 6 }); test('returns false and correct byte length when mixed string byte length equals limit', () => { - expect(isValidInputLength('aक😊', 8)).toEqual({isValid: false, byteLength: 8}); // 1 + 3 + 4 = 8 bytes ≤ 8 + expect(isValidInputLength('aक😊', 8)).toEqual({isValid: true, byteLength: 8}); // 1 + 3 + 4 = 8 bytes ≤ 8 }); test('returns false and correct byte length when mixed string byte length is less than limit', () => { - expect(isValidInputLength('aक', 5)).toEqual({isValid: false, byteLength: 4}); // 1 + 3 = 4 bytes ≤ 5 + expect(isValidInputLength('aक', 5)).toEqual({isValid: true, byteLength: 4}); // 1 + 3 = 4 bytes ≤ 5 }); }); // Test edge cases describe('Edge cases', () => { test('handles negative length parameter', () => { - expect(isValidInputLength('abc', -1)).toEqual({isValid: true, byteLength: 3}); // 3 bytes > -1 + expect(isValidInputLength('abc', -1)).toEqual({isValid: false, byteLength: 3}); // 3 bytes > -1 }); test('handles zero length parameter', () => { - expect(isValidInputLength('a', 0)).toEqual({isValid: true, byteLength: 1}); // 1 byte > 0 - expect(isValidInputLength('', 0)).toEqual({isValid: false, byteLength: 0}); // 0 bytes ≤ 0 + expect(isValidInputLength('a', 0)).toEqual({isValid: false, byteLength: 1}); // 1 byte > 0 + expect(isValidInputLength('', 0)).toEqual({isValid: true, byteLength: 0}); // 0 bytes ≤ 0 }); test('handles special characters (e.g., newlines, tabs)', () => { - expect(isValidInputLength('\n\t', 1)).toEqual({isValid: true, byteLength: 2}); // 2 bytes > 1 - expect(isValidInputLength('\n\t', 2)).toEqual({isValid: false, byteLength: 2}); // 2 bytes ≤ 2 + expect(isValidInputLength('\n\t', 1)).toEqual({isValid: false, byteLength: 2}); // 2 bytes > 1 + expect(isValidInputLength('\n\t', 2)).toEqual({isValid: true, byteLength: 2}); // 2 bytes ≤ 2 }); }); });