From 7b792836091e3696cf8943c4c2fa5f53e49009d6 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Fri, 22 Aug 2025 16:54:39 +0800 Subject: [PATCH 1/2] revert diff --- cspell.json | 1 - src/CONST/index.ts | 4 - src/libs/API/index.ts | 26 +- src/libs/Formula.ts | 505 ------------------ src/libs/OptimisticReportNames.ts | 390 -------------- .../OptimisticReportNamesConnectionManager.ts | 137 ----- src/libs/Permissions.ts | 7 +- tests/perf-test/Formula.perf-test.ts | 143 ----- .../OptimisticReportNames.perf-test.ts | 283 ---------- tests/unit/FormulaTest.ts | 360 ------------- tests/unit/OptimisticReportNamesTest.ts | 433 --------------- 11 files changed, 3 insertions(+), 2286 deletions(-) delete mode 100644 src/libs/Formula.ts delete mode 100644 src/libs/OptimisticReportNames.ts delete mode 100644 src/libs/OptimisticReportNamesConnectionManager.ts delete mode 100644 tests/perf-test/Formula.perf-test.ts delete mode 100644 tests/perf-test/OptimisticReportNames.perf-test.ts delete mode 100644 tests/unit/FormulaTest.ts delete mode 100644 tests/unit/OptimisticReportNamesTest.ts diff --git a/cspell.json b/cspell.json index dfaf6a68fc8f2..278ee5a01c845 100644 --- a/cspell.json +++ b/cspell.json @@ -239,7 +239,6 @@ "formatjs", "Français", "Frederico", - "freetext", "frontpart", "fullstory", "FWTV", diff --git a/src/CONST/index.ts b/src/CONST/index.ts index b79bf4f1852b4..9f83e991328ed 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -664,7 +664,6 @@ const CONST = { BETAS: { ALL: 'all', ASAP_SUBMIT: 'asapSubmit', - AUTH_AUTO_REPORT_TITLE: 'authAutoReportTitle', DEFAULT_ROOMS: 'defaultRooms', P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests', SPOTNANA_TRAVEL: 'spotnanaTravel', @@ -1528,9 +1527,6 @@ const CONST = { APPLY_AIRSHIP_UPDATES: 'apply_airship_updates', APPLY_PUSHER_UPDATES: 'apply_pusher_updates', APPLY_HTTPS_UPDATES: 'apply_https_updates', - COMPUTE_REPORT_NAME: 'compute_report_name', - COMPUTE_REPORT_NAME_FOR_NEW_REPORT: 'compute_report_name_for_new_report', - UPDATE_OPTIMISTIC_REPORT_NAMES: 'update_optimistic_report_names', COLD: 'cold', WARM: 'warm', REPORT_ACTION_ITEM_LAYOUT_DEBOUNCE_TIME: 1500, diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 4157c20e04902..50225e8c232ec 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -7,8 +7,6 @@ import Log from '@libs/Log'; import {handleDeletedAccount, HandleUnusedOptimisticID, Logging, Pagination, Reauthentication, RecheckConnection, SaveResponseInOnyx} from '@libs/Middleware'; import {isOffline} from '@libs/Network/NetworkStore'; import {push as pushToSequentialQueue, waitForIdle as waitForSequentialQueueIdle} from '@libs/Network/SequentialQueue'; -import * as OptimisticReportNames from '@libs/OptimisticReportNames'; -import {getUpdateContext, initialize as initializeOptimisticReportNamesContext} from '@libs/OptimisticReportNamesConnectionManager'; import Pusher from '@libs/Pusher'; import {processWithMiddleware, use} from '@libs/Request'; import {getAll, getLength as getPersistedRequestsLength} from '@userActions/PersistedRequests'; @@ -17,7 +15,7 @@ import type OnyxRequest from '@src/types/onyx/Request'; import type {PaginatedRequest, PaginationConfig, RequestConflictResolver} from '@src/types/onyx/Request'; import type Response from '@src/types/onyx/Response'; import type {ApiCommand, ApiRequestCommandParameters, ApiRequestType, CommandOfType, ReadCommand, SideEffectRequestCommand, WriteCommand} from './types'; -import {READ_COMMANDS, WRITE_COMMANDS} from './types'; +import {READ_COMMANDS} from './types'; // Setup API middlewares. Each request made will pass through a series of middleware functions that will get called in sequence (each one passing the result of the previous to the next). // Note: The ordering here is intentional as we want to Log, Recheck Connection, Reauthenticate, and Save the Response in Onyx. Errors thrown in one middleware will bubble to the next. @@ -44,11 +42,6 @@ use(Pagination); // middlewares after this, because the SequentialQueue depends on the result of this middleware to pause the queue (if needed) to bring the app to an up-to-date state. use(SaveResponseInOnyx); -// Initialize OptimisticReportNames context on module load -initializeOptimisticReportNamesContext().catch(() => { - Log.warn('Failed to initialize OptimisticReportNames context'); -}); - let requestIndex = 0; type OnyxData = { @@ -81,22 +74,7 @@ function prepareRequest( const {optimisticData, ...onyxDataWithoutOptimisticData} = onyxData; if (optimisticData && shouldApplyOptimisticData) { Log.info('[API] Applying optimistic data', false, {command, type}); - - // Process optimistic data through report name middleware - // Skip for OpenReport command to avoid unnecessary processing - if (command === WRITE_COMMANDS.OPEN_REPORT) { - Onyx.update(optimisticData); - } else { - try { - const context = getUpdateContext(); - const processedOptimisticData = OptimisticReportNames.updateOptimisticReportNamesFromUpdates(optimisticData, context); - Onyx.update(processedOptimisticData); - } catch (error) { - Log.warn('[API] Failed to process optimistic report names', {error}); - // Fallback to original optimistic data if processing fails - Onyx.update(optimisticData); - } - } + Onyx.update(optimisticData); } const isWriteRequest = type === CONST.API_REQUEST_TYPE.WRITE; diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts deleted file mode 100644 index 96627bb284ecc..0000000000000 --- a/src/libs/Formula.ts +++ /dev/null @@ -1,505 +0,0 @@ -import type {OnyxEntry} from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; -import CONST from '@src/CONST'; -import type {Policy, Report, Transaction} from '@src/types/onyx'; -import {getCurrencySymbol} from './CurrencyUtils'; -import {getAllReportActions} from './ReportActionsUtils'; -import {getReportTransactions} from './ReportUtils'; -import {getCreated, isPartialTransaction} from './TransactionUtils'; - -type FormulaPart = { - /** The original definition from the formula */ - definition: string; - - /** The type of formula part (report, field, user, etc.) */ - type: ValueOf; - - /** The field path for accessing data (e.g., ['type'], ['startdate'], ['total']) */ - fieldPath: string[]; - - /** Functions to apply to the computed value (e.g., ['frontPart']) */ - functions: string[]; -}; - -type FormulaContext = { - report: Report; - policy: OnyxEntry; - transaction?: Transaction; -}; - -const FORMULA_PART_TYPES = { - REPORT: 'report', - FIELD: 'field', - USER: 'user', - FREETEXT: 'freetext', -} as const; - -/** - * Extract formula parts from a formula string, handling nested braces and escapes - * Based on OldDot Formula.extract method - */ -function extract(formula: string, opener = '{', closer = '}'): string[] { - if (!formula || typeof formula !== 'string') { - return []; - } - - const letters = formula.split(''); - const sections: string[] = []; - let nesting = 0; - let start = 0; - - for (let i = 0; i < letters.length; i++) { - // Found an escape character, skip the next character - if (letters.at(i) === '\\') { - i++; - continue; - } - - // Found an opener, save the spot - if (letters.at(i) === opener) { - if (nesting === 0) { - start = i; - } - nesting++; - } - - // Found a closer, decrement the nesting and possibly extract it - if (letters.at(i) === closer && nesting > 0) { - nesting--; - if (nesting === 0) { - sections.push(formula.substring(start, i + 1)); - } - } - } - - return sections; -} - -/** - * Parse a formula string into an array of formula parts - * Based on OldDot Formula.parse method - */ -function parse(formula: string): FormulaPart[] { - if (!formula || typeof formula !== 'string') { - return []; - } - - const parts: FormulaPart[] = []; - const formulaParts = extract(formula); - - // If no formula parts found, treat the entire string as free text - if (formulaParts.length === 0) { - if (formula.trim()) { - parts.push({ - definition: formula, - type: FORMULA_PART_TYPES.FREETEXT, - fieldPath: [], - functions: [], - }); - } - return parts; - } - - // Process the formula by splitting on formula parts to preserve free text - let lastIndex = 0; - - formulaParts.forEach((part) => { - const partIndex = formula.indexOf(part, lastIndex); - - // Add any free text before this formula part - if (partIndex > lastIndex) { - const freeText = formula.substring(lastIndex, partIndex); - if (freeText) { - parts.push({ - definition: freeText, - type: FORMULA_PART_TYPES.FREETEXT, - fieldPath: [], - functions: [], - }); - } - } - - // Add the formula part - parts.push(parsePart(part)); - lastIndex = partIndex + part.length; - }); - - // Add any remaining free text after the last formula part - if (lastIndex < formula.length) { - const freeText = formula.substring(lastIndex); - if (freeText) { - parts.push({ - definition: freeText, - type: FORMULA_PART_TYPES.FREETEXT, - fieldPath: [], - functions: [], - }); - } - } - - return parts; -} - -/** - * Parse a single formula part definition into a FormulaPart object - * Based on OldDot Formula.parsePart method - */ -function parsePart(definition: string): FormulaPart { - const part: FormulaPart = { - definition, - type: FORMULA_PART_TYPES.FREETEXT, - fieldPath: [], - functions: [], - }; - - // If it doesn't start and end with braces, it's free text - if (!definition.startsWith('{') || !definition.endsWith('}')) { - return part; - } - - // Remove the braces and trim - const cleanDefinition = definition.slice(1, -1).trim(); - if (!cleanDefinition) { - return part; - } - - // Split on | to separate functions - const segments = cleanDefinition.split('|'); - const fieldSegment = segments.at(0); - const functions = segments.slice(1); - - // Split the field segment on : to get the field path - const fieldPath = fieldSegment?.split(':'); - const type = fieldPath?.at(0)?.toLowerCase(); - - // Determine the formula part type - if (type === 'report') { - part.type = FORMULA_PART_TYPES.REPORT; - } else if (type === 'field') { - part.type = FORMULA_PART_TYPES.FIELD; - } else if (type === 'user') { - part.type = FORMULA_PART_TYPES.USER; - } - - // Set field path (excluding the type) - part.fieldPath = fieldPath?.slice(1) ?? []; - part.functions = functions; - - return part; -} - -/** - * Compute the value of a formula given a context - */ -function compute(formula: string, context: FormulaContext): string { - if (!formula || typeof formula !== 'string') { - return ''; - } - - const parts = parse(formula); - let result = ''; - - for (const part of parts) { - let value = ''; - - switch (part.type) { - case FORMULA_PART_TYPES.REPORT: - value = computeReportPart(part, context); - value = value === '' ? part.definition : value; - break; - case FORMULA_PART_TYPES.FIELD: - value = computeFieldPart(part); - break; - case FORMULA_PART_TYPES.USER: - value = computeUserPart(part); - break; - case FORMULA_PART_TYPES.FREETEXT: - value = part.definition; - break; - default: - // If we don't recognize the part type, use the original definition - value = part.definition; - } - - // Apply any functions to the computed value - value = applyFunctions(value, part.functions); - result += value; - } - - return result; -} - -/** - * Compute the value of a report formula part - */ -function computeReportPart(part: FormulaPart, context: FormulaContext): string { - const {report, policy} = context; - const [field, format] = part.fieldPath; - - if (!field) { - return part.definition; - } - - switch (field.toLowerCase()) { - case 'type': - return formatType(report.type); - case 'startdate': - return formatDate(getOldestTransactionDate(report.reportID, context), format); - case 'total': - return formatAmount(report.total, getCurrencySymbol(report.currency ?? '') ?? report.currency); - case 'currency': - return report.currency ?? ''; - case 'policyname': - case 'workspacename': - return policy?.name ?? ''; - case 'created': - // Backend will always return at least one report action (of type created) and its date is equal to report's creation date - // We can make it slightly more efficient in the future by ensuring report.created is always present in backend's responses - return formatDate(getOldestReportActionDate(report.reportID), format); - default: - return part.definition; - } -} - -/** - * Compute the value of a field formula part - */ -function computeFieldPart(part: FormulaPart): string { - // Field computation will be implemented later - return part.definition; -} - -/** - * Compute the value of a user formula part - */ -function computeUserPart(part: FormulaPart): string { - // User computation will be implemented later - return part.definition; -} - -/** - * Apply functions to a computed value - */ -function applyFunctions(value: string, functions: string[]): string { - let result = value; - - for (const func of functions) { - const [functionName, ...args] = func.split(':'); - - switch (functionName.toLowerCase()) { - case 'frontpart': - result = getFrontPart(result); - break; - case 'substr': - result = getSubstring(result, args); - break; - case 'domain': - result = getDomainName(result); - break; - default: - // Unknown function, leave value as is - break; - } - } - - return result; -} - -/** - * Get the front part of an email or first word of a string - */ -function getFrontPart(value: string): string { - const trimmed = value.trim(); - - // If it's an email, return the part before @ - if (trimmed.includes('@')) { - return trimmed.split('@').at(0) ?? ''; - } - - // Otherwise, return the first word - return trimmed.split(' ').at(0) ?? ''; -} - -/** - * Get the domain name of an email or URL - */ -function getDomainName(value: string): string { - const trimmed = value.trim(); - - // If it's an email, return the part after @ - if (trimmed.includes('@')) { - return trimmed.split('@').at(1) ?? ''; - } - - return ''; -} - -/** - * Get substring of a value - */ -function getSubstring(value: string, args: string[]): string { - const start = parseInt(args.at(0) ?? '', 10) || 0; - const length = args.at(1) ? parseInt(args.at(1) ?? '', 10) : undefined; - - if (length !== undefined) { - return value.substring(start, start + length); - } - - return value.substring(start); -} - -/** - * Format a date value with support for multiple date formats - */ -function formatDate(dateString: string | undefined, format = 'yyyy-MM-dd'): string { - if (!dateString) { - return ''; - } - - try { - const date = new Date(dateString); - if (Number.isNaN(date.getTime())) { - return ''; - } - - const year = date.getFullYear(); - const month = date.getMonth() + 1; - const day = date.getDate(); - const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; - const shortMonthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; - - switch (format) { - case 'M/dd/yyyy': - return `${month}/${day.toString().padStart(2, '0')}/${year}`; - case 'MMMM dd, yyyy': - return `${monthNames.at(month - 1)} ${day.toString().padStart(2, '0')}, ${year}`; - case 'dd MMM yyyy': - return `${day.toString().padStart(2, '0')} ${shortMonthNames.at(month - 1)} ${year}`; - case 'yyyy/MM/dd': - return `${year}/${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}`; - case 'MMMM, yyyy': - return `${monthNames.at(month - 1)}, ${year}`; - case 'yy/MM/dd': - return `${year.toString().slice(-2)}/${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}`; - case 'dd/MM/yy': - return `${day.toString().padStart(2, '0')}/${month.toString().padStart(2, '0')}/${year.toString().slice(-2)}`; - case 'yyyy': - return year.toString(); - case 'MM/dd/yyyy': - return `${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}/${year}`; - case 'yyyy-MM-dd': - default: - return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`; - } - } catch { - return ''; - } -} - -/** - * Format an amount value - */ -function formatAmount(amount: number | undefined, currency: string | undefined): string { - if (amount === undefined) { - return ''; - } - - const absoluteAmount = Math.abs(amount); - const formattedAmount = (absoluteAmount / 100).toFixed(2); - - if (currency) { - return `${currency}${formattedAmount}`; - } - - return formattedAmount; -} - -/** - * Get the date of the oldest report action for a given report - */ -function getOldestReportActionDate(reportID: string): string | undefined { - if (!reportID) { - return undefined; - } - - const reportActions = getAllReportActions(reportID); - if (!reportActions || Object.keys(reportActions).length === 0) { - return undefined; - } - - let oldestDate: string | undefined; - - Object.values(reportActions).forEach((action) => { - if (!action?.created) { - return; - } - - if (oldestDate && action.created > oldestDate) { - return; - } - oldestDate = action.created; - }); - - return oldestDate; -} - -/** - * Format a report type to its human-readable string - */ -function formatType(type: string | undefined): string { - if (!type) { - return ''; - } - - const typeMapping: Record = { - [CONST.REPORT.TYPE.EXPENSE]: 'Expense Report', - [CONST.REPORT.TYPE.INVOICE]: 'Invoice', - [CONST.REPORT.TYPE.CHAT]: 'Chat', - [CONST.REPORT.UNSUPPORTED_TYPE.BILL]: 'Bill', - [CONST.REPORT.UNSUPPORTED_TYPE.PAYCHECK]: 'Paycheck', - [CONST.REPORT.TYPE.IOU]: 'IOU', - [CONST.REPORT.TYPE.TASK]: 'Task', - trip: 'Trip', - }; - - return typeMapping[type.toLowerCase()] || type; -} - -/** - * Get the date of the oldest transaction for a given report - */ -function getOldestTransactionDate(reportID: string, context?: FormulaContext): string | undefined { - if (!reportID) { - return undefined; - } - - const transactions = getReportTransactions(reportID); - if (!transactions || transactions.length === 0) { - return new Date().toISOString(); - } - - let oldestDate: string | undefined; - - transactions.forEach((transaction) => { - // Use updated transaction data if available and matches this transaction - const currentTransaction = context?.transaction && transaction.transactionID === context.transaction.transactionID ? context.transaction : transaction; - - const created = getCreated(currentTransaction); - if (!created) { - return; - } - if (oldestDate && created >= oldestDate) { - return; - } - if (isPartialTransaction(currentTransaction)) { - return; - } - oldestDate = created; - }); - - return oldestDate; -} - -export {FORMULA_PART_TYPES, compute, extract, parse}; - -export type {FormulaContext, FormulaPart}; diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts deleted file mode 100644 index 7e37ad632e295..0000000000000 --- a/src/libs/OptimisticReportNames.ts +++ /dev/null @@ -1,390 +0,0 @@ -import type {OnyxUpdate} from 'react-native-onyx'; -import Onyx from 'react-native-onyx'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {OnyxKey} from '@src/ONYXKEYS'; -import type {Transaction} from '@src/types/onyx'; -import type Policy from '@src/types/onyx/Policy'; -import type Report from '@src/types/onyx/Report'; -import Timing from './actions/Timing'; -import {compute, FORMULA_PART_TYPES, parse} from './Formula'; -import type {FormulaContext} from './Formula'; -import Log from './Log'; -import {getUpdateContextAsync} from './OptimisticReportNamesConnectionManager'; -import type {UpdateContext} from './OptimisticReportNamesConnectionManager'; -import Performance from './Performance'; -import Permissions from './Permissions'; -import {getTitleReportField, isArchivedReport} from './ReportUtils'; - -/** - * Get the object type from an Onyx key - */ -function determineObjectTypeByKey(key: string): 'report' | 'policy' | 'transaction' | 'unknown' { - if (key.startsWith(ONYXKEYS.COLLECTION.REPORT)) { - return 'report'; - } - if (key.startsWith(ONYXKEYS.COLLECTION.POLICY)) { - return 'policy'; - } - if (key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)) { - return 'transaction'; - } - return 'unknown'; -} - -/** - * Extract report ID from an Onyx key - */ -function getReportIDFromKey(key: string): string { - return key.replace(ONYXKEYS.COLLECTION.REPORT, ''); -} - -/** - * Extract policy ID from an Onyx key - */ -function getPolicyIDFromKey(key: string): string { - return key.replace(ONYXKEYS.COLLECTION.POLICY, ''); -} - -/** - * Extract transaction ID from an Onyx key - */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -- this will be used in near future -function getTransactionIDFromKey(key: string): string { - return key.replace(ONYXKEYS.COLLECTION.TRANSACTION, ''); -} - -/** - * Get report by ID from the reports collection - */ -function getReportByID(reportID: string, allReports: Record): Report | undefined { - return allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; -} - -/** - * Get policy by ID from the policies collection - */ -function getPolicyByID(policyID: string | undefined, allPolicies: Record): Policy | undefined { - if (!policyID) { - return; - } - return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; -} - -/** - * Get transaction by ID from the transactions collection - */ -function getTransactionByID(transactionID: string, allTransactions: Record): Transaction | undefined { - return allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; -} - -/** - * Get all reports associated with a policy ID - */ -function getReportsByPolicyID(policyID: string, allReports: Record, context: UpdateContext): Report[] { - if (policyID === CONST.POLICY.ID_FAKE) { - return []; - } - return Object.values(allReports).filter((report) => { - if (report?.policyID !== policyID) { - return false; - } - - // Filter by type - only reports that support custom names - if (!isValidReportType(report.type)) { - return false; - } - - // Filter by state - exclude reports in high states (like approved or higher) - const stateThreshold = CONST.REPORT.STATE_NUM.APPROVED; - if (report.stateNum && report.stateNum > stateThreshold) { - return false; - } - - // Filter by isArchived - exclude archived reports - const reportNameValuePairs = context.allReportNameValuePairs[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`]; - if (isArchivedReport(reportNameValuePairs)) { - return false; - } - - return true; - }); -} - -/** - * Get the report associated with a transaction ID - */ -function getReportByTransactionID(transactionID: string, context: UpdateContext): Report | undefined { - if (!transactionID) { - return undefined; - } - - const transaction = getTransactionByID(transactionID, context.allTransactions); - - if (!transaction?.reportID) { - return undefined; - } - - // Get the report using the transaction's reportID from context - return getReportByID(transaction.reportID, context.allReports); -} - -/** - * Generate the Onyx key for a report - */ -function getReportKey(reportID: string): OnyxKey { - return `${ONYXKEYS.COLLECTION.REPORT}${reportID}` as OnyxKey; -} - -/** - * Check if a report should have its name automatically computed - */ -function shouldComputeReportName(report: Report, policy: Policy | undefined): boolean { - // Only compute names for expense reports with policies that have title fields - if (!report || !policy) { - return false; - } - - // Check if the report is an expense report - if (!isValidReportType(report.type)) { - return false; - } - - // Check if the policy has a title field with a formula - const titleField = getTitleReportField(policy.fieldList ?? {}); - if (!titleField?.defaultValue) { - return false; - } - return true; -} - -function isValidReportType(reportType?: string): boolean { - if (!reportType) { - return false; - } - return ( - reportType === CONST.REPORT.TYPE.EXPENSE || - reportType === CONST.REPORT.TYPE.INVOICE || - reportType === CONST.REPORT.UNSUPPORTED_TYPE.BILL || - reportType === CONST.REPORT.UNSUPPORTED_TYPE.PAYCHECK || - reportType === 'trip' - ); -} - -/** - * Compute a new report name if needed based on an optimistic update - */ -function computeReportNameIfNeeded(report: Report | undefined, incomingUpdate: OnyxUpdate, context: UpdateContext): string | null { - Performance.markStart(CONST.TIMING.COMPUTE_REPORT_NAME); - Timing.start(CONST.TIMING.COMPUTE_REPORT_NAME); - - const {allPolicies} = context; - - // If no report is provided, extract it from the update (for new reports) - const targetReport = report ?? (incomingUpdate.value as Report); - - if (!targetReport) { - Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME); - Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME); - return null; - } - - const policy = getPolicyByID(targetReport.policyID, allPolicies); - - if (!shouldComputeReportName(targetReport, policy)) { - Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME); - Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME); - return null; - } - - const titleField = getTitleReportField(policy?.fieldList ?? {}); - if (!titleField?.defaultValue) { - Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME); - Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME); - return null; - } - - // Quick check: see if the update might affect the report name - const updateType = determineObjectTypeByKey(incomingUpdate.key); - const formula = titleField.defaultValue; - const formulaParts = parse(formula); - - let transaction: Transaction | undefined; - if (updateType === 'transaction') { - transaction = getTransactionByID((incomingUpdate.value as Transaction).transactionID, context.allTransactions); - } - - // Check if any formula part might be affected by this update - const isAffected = formulaParts.some((part) => { - if (part.type === FORMULA_PART_TYPES.REPORT) { - // Checking if the formula part is affected in this manner works, but it could certainly be more precise. - // For example, a policy update only affects the part if the formula in the policy changed, or if the report part references a field on the policy. - // However, if we run into performance problems, this would be a good place to optimize. - return updateType === 'report' || updateType === 'transaction' || updateType === 'policy'; - } - if (part.type === FORMULA_PART_TYPES.FIELD) { - return updateType === 'report'; - } - return false; - }); - - if (!isAffected) { - Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME); - Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME); - return null; - } - - // Build context with the updated data - const updatedReport = - updateType === 'report' && targetReport.reportID === getReportIDFromKey(incomingUpdate.key) ? {...targetReport, ...(incomingUpdate.value as Partial)} : targetReport; - - const updatedPolicy = updateType === 'policy' && targetReport.policyID === getPolicyIDFromKey(incomingUpdate.key) ? {...(policy ?? {}), ...(incomingUpdate.value as Policy)} : policy; - - const updatedTransaction = updateType === 'transaction' ? {...(transaction ?? {}), ...(incomingUpdate.value as Transaction)} : undefined; - - // Compute the new name - const formulaContext: FormulaContext = { - report: updatedReport, - policy: updatedPolicy, - transaction: updatedTransaction, - }; - - const newName = compute(formula, formulaContext); - - // Only return an update if the name actually changed - if (newName && newName !== targetReport.reportName) { - Log.info('[OptimisticReportNames] Report name computed', false, { - reportID: targetReport.reportID, - oldName: targetReport.reportName, - newName, - formula, - updateType, - isNewReport: !report, - }); - - Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME); - Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME); - return newName; - } - - Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME); - Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME); - return null; -} - -/** - * Update optimistic report names based on incoming updates - * This is the main middleware function that processes optimistic data - */ -function updateOptimisticReportNamesFromUpdates(updates: OnyxUpdate[], context: UpdateContext): OnyxUpdate[] { - Performance.markStart(CONST.TIMING.UPDATE_OPTIMISTIC_REPORT_NAMES); - Timing.start(CONST.TIMING.UPDATE_OPTIMISTIC_REPORT_NAMES); - - const {betas, allReports} = context; - - // Check if the feature is enabled - if (!Permissions.canUseCustomReportNames(betas)) { - Performance.markEnd(CONST.TIMING.UPDATE_OPTIMISTIC_REPORT_NAMES); - Timing.end(CONST.TIMING.UPDATE_OPTIMISTIC_REPORT_NAMES); - return updates; - } - - Log.info('[OptimisticReportNames] Processing optimistic updates for report names', false, { - updatesCount: updates.length, - }); - - const additionalUpdates: OnyxUpdate[] = []; - - for (const update of updates) { - const objectType = determineObjectTypeByKey(update.key); - - switch (objectType) { - case 'report': { - const reportID = getReportIDFromKey(update.key); - const report = getReportByID(reportID, allReports); - - // Handle both existing and new reports with the same function - const reportNameUpdate = computeReportNameIfNeeded(report, update, context); - - if (reportNameUpdate) { - additionalUpdates.push({ - key: getReportKey(reportID), - onyxMethod: Onyx.METHOD.MERGE, - value: { - reportName: reportNameUpdate, - }, - }); - } - break; - } - - case 'policy': { - const policyID = getPolicyIDFromKey(update.key); - const affectedReports = getReportsByPolicyID(policyID, allReports, context); - for (const report of affectedReports) { - const reportNameUpdate = computeReportNameIfNeeded(report, update, context); - - if (reportNameUpdate) { - additionalUpdates.push({ - key: getReportKey(report.reportID), - onyxMethod: Onyx.METHOD.MERGE, - value: { - reportName: reportNameUpdate, - }, - }); - } - } - break; - } - - case 'transaction': { - let report: Report | undefined; - const transactionUpdate = update.value as Partial; - if (transactionUpdate.reportID) { - report = getReportByID(transactionUpdate.reportID, allReports); - } else { - report = getReportByTransactionID(getTransactionIDFromKey(update.key), context); - } - - if (report) { - const reportNameUpdate = computeReportNameIfNeeded(report, update, context); - - if (reportNameUpdate) { - additionalUpdates.push({ - key: getReportKey(report.reportID), - onyxMethod: Onyx.METHOD.MERGE, - value: { - reportName: reportNameUpdate, - }, - }); - } - } - break; - } - - default: - continue; - } - } - - Log.info('[OptimisticReportNames] Processing completed', false, { - additionalUpdatesCount: additionalUpdates.length, - totalUpdatesReturned: updates.length + additionalUpdates.length, - }); - - Performance.markEnd(CONST.TIMING.UPDATE_OPTIMISTIC_REPORT_NAMES); - Timing.end(CONST.TIMING.UPDATE_OPTIMISTIC_REPORT_NAMES); - - return updates.concat(additionalUpdates); -} - -/** - * Creates update context for optimistic report name processing. - * This should be called before processing optimistic updates - */ -function createUpdateContext(): Promise { - return getUpdateContextAsync(); -} - -export {updateOptimisticReportNamesFromUpdates, computeReportNameIfNeeded, createUpdateContext, shouldComputeReportName, getReportByTransactionID}; -export type {UpdateContext}; diff --git a/src/libs/OptimisticReportNamesConnectionManager.ts b/src/libs/OptimisticReportNamesConnectionManager.ts deleted file mode 100644 index 39a36e687659e..0000000000000 --- a/src/libs/OptimisticReportNamesConnectionManager.ts +++ /dev/null @@ -1,137 +0,0 @@ -import type {OnyxEntry} from 'react-native-onyx'; -import Onyx from 'react-native-onyx'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {Transaction} from '@src/types/onyx'; -import type Beta from '@src/types/onyx/Beta'; -import type Policy from '@src/types/onyx/Policy'; -import type Report from '@src/types/onyx/Report'; -import type ReportNameValuePairs from '@src/types/onyx/ReportNameValuePairs'; - -type UpdateContext = { - betas: OnyxEntry; - allReports: Record; - allPolicies: Record; - allReportNameValuePairs: Record; - allTransactions: Record; -}; - -let betas: OnyxEntry; -let allReports: Record; -let allPolicies: Record; -let allReportNameValuePairs: Record; -let allTransactions: Record; -let isInitialized = false; -let connectionsInitializedCount = 0; -const totalConnections = 5; -let initializationPromise: Promise | null = null; - -/** - * Initialize persistent connections to Onyx data needed for OptimisticReportNames - * This is called lazily when OptimisticReportNames functionality is first used - * Returns a Promise that resolves when all connections have received their initial data - * - * We use Onyx.connectWithoutView because we do not use this in React components and this logic is not tied to the UI. - * This is a centralized system that needs access to all objects of several types, so that when any updates affect - * the computed report names, we can compute the new names according to the formula and add the necessary updates. - * It wouldn't be possible to do this without connecting to all the data. - * - */ -function initialize(): Promise { - if (isInitialized) { - return Promise.resolve(); - } - - if (initializationPromise) { - return initializationPromise; - } - - initializationPromise = new Promise((resolve) => { - const checkAndMarkInitialized = () => { - connectionsInitializedCount++; - if (connectionsInitializedCount === totalConnections) { - isInitialized = true; - resolve(); - } - }; - - // Connect to BETAS - Onyx.connectWithoutView({ - key: ONYXKEYS.BETAS, - callback: (val) => { - betas = val; - checkAndMarkInitialized(); - }, - }); - - // Connect to all REPORTS - Onyx.connectWithoutView({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (val) => { - allReports = (val as Record) ?? {}; - checkAndMarkInitialized(); - }, - }); - - // Connect to all POLICIES - Onyx.connectWithoutView({ - key: ONYXKEYS.COLLECTION.POLICY, - waitForCollectionCallback: true, - callback: (val) => { - allPolicies = (val as Record) ?? {}; - checkAndMarkInitialized(); - }, - }); - - // Connect to all REPORT_NAME_VALUE_PAIRS - Onyx.connectWithoutView({ - key: ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, - waitForCollectionCallback: true, - callback: (val) => { - allReportNameValuePairs = (val as Record) ?? {}; - checkAndMarkInitialized(); - }, - }); - - // Connect to all TRANSACTIONS - Onyx.connectWithoutView({ - key: ONYXKEYS.COLLECTION.TRANSACTION, - waitForCollectionCallback: true, - callback: (val) => { - allTransactions = (val as Record) ?? {}; - checkAndMarkInitialized(); - }, - }); - }); - - return initializationPromise; -} - -/** - * Get the current update context synchronously - * Must be called after initialize() has completed - */ -function getUpdateContext(): UpdateContext { - if (!isInitialized) { - throw new Error('OptimisticReportNamesConnectionManager not initialized. Call initialize() first.'); - } - - return { - betas, - allReports: allReports ?? {}, - allPolicies: allPolicies ?? {}, - allReportNameValuePairs: allReportNameValuePairs ?? {}, - allTransactions: allTransactions ?? {}, - }; -} - -/** - * Get the current update context as a promise for backward compatibility - * Initializes connections lazily on first use - */ -function getUpdateContextAsync(): Promise { - return initialize().then(() => getUpdateContext()); -} - -export {initialize, getUpdateContext, getUpdateContextAsync}; -export type {UpdateContext}; diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index cc36adff7ec93..979c0df7e252b 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -7,11 +7,6 @@ function canUseAllBetas(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.ALL); } -// eslint-disable-next-line rulesdir/no-beta-handler -function canUseCustomReportNames(betas: OnyxEntry): boolean { - return isBetaEnabled(CONST.BETAS.AUTH_AUTO_REPORT_TITLE, betas); -} - // eslint-disable-next-line rulesdir/no-beta-handler function isBlockedFromSpotnanaTravel(betas: OnyxEntry): boolean { // Don't check for all betas or nobody can use test travel on dev @@ -35,8 +30,8 @@ function isBetaEnabled(beta: Beta, betas: OnyxEntry): boolean { } export default { - canUseCustomReportNames, canUseLinkPreviews, isBlockedFromSpotnanaTravel, isBetaEnabled, + canUseAllBetas, }; diff --git a/tests/perf-test/Formula.perf-test.ts b/tests/perf-test/Formula.perf-test.ts deleted file mode 100644 index dfe574172d326..0000000000000 --- a/tests/perf-test/Formula.perf-test.ts +++ /dev/null @@ -1,143 +0,0 @@ -import {measureFunction} from 'reassure'; -import {compute, extract, parse} from '@libs/Formula'; -import type {FormulaContext} from '@libs/Formula'; -import type {Policy, Report} from '@src/types/onyx'; - -describe('[CustomFormula] Performance Tests', () => { - const mockReport = { - reportID: '123', - reportName: 'Test Report', - total: -10000, - currency: 'USD', - lastVisibleActionCreated: '2025-01-15T10:30:00Z', - policyID: 'policy1', - } as Report; - - const mockPolicy = { - name: 'Test Policy', - id: 'policy1', - } as Policy; - - const mockContext: FormulaContext = { - report: mockReport, - policy: mockPolicy, - }; - - describe('Formula Parsing Performance', () => { - test('[CustomFormula] extract() with simple formula', async () => { - const formula = '{report:type} - {report:total}'; - await measureFunction(() => extract(formula)); - }); - - test('[CustomFormula] extract() with complex formula', async () => { - const formula = '{report:type} - {report:startdate} - {report:total} - {report:currency} - {report:policyname} - {user:email|frontPart}'; - await measureFunction(() => extract(formula)); - }); - - test('[CustomFormula] extract() with nested braces', async () => { - const formula = '{report:{report:submit:from:firstName|substr:2}}'; - await measureFunction(() => extract(formula)); - }); - - test('[CustomFormula] parse() with simple formula', async () => { - const formula = '{report:type} - {report:total}'; - await measureFunction(() => parse(formula)); - }); - - test('[CustomFormula] parse() with complex formula', async () => { - const formula = '{report:type} - {report:startdate} - {report:total} - {report:currency} - {report:policyname} - {field:department} - {user:email|frontPart}'; - await measureFunction(() => parse(formula)); - }); - - test('[CustomFormula] parse() with mixed content', async () => { - const formula = 'Expense Report for {report:policyname} on {report:startdate} totaling {report:total} {report:currency} submitted by {user:email|frontPart}'; - await measureFunction(() => parse(formula)); - }); - }); - - describe('Formula Computation Performance', () => { - test('[CustomFormula] compute() with simple formula', async () => { - const formula = '{report:type} - {report:total}'; - await measureFunction(() => compute(formula, mockContext)); - }); - - test('[CustomFormula] compute() with complex formula', async () => { - const formula = '{report:type} - {report:startdate} - {report:total} - {report:currency} - {report:policyname}'; - await measureFunction(() => compute(formula, mockContext)); - }); - - test('[CustomFormula] compute() with mixed content', async () => { - const formula = 'Expense Report for {report:policyname} on {report:startdate} totaling {report:total} {report:currency}'; - await measureFunction(() => compute(formula, mockContext)); - }); - - test('[CustomFormula] compute() with missing data context', async () => { - const formula = '{report:type} - {report:total} - {report:unknown} - {report:policyname}'; - const contextWithMissingData: FormulaContext = { - report: {} as Report, - policy: null as unknown as Policy, - }; - await measureFunction(() => compute(formula, contextWithMissingData)); - }); - }); - - describe('Batch Processing Performance', () => { - test('[CustomFormula] parse() batch processing 100 formulas', async () => { - const formulas = Array.from({length: 100}, (_, i) => `{report:type} ${i} - {report:startdate} - {report:total} - {report:currency}`); - - await measureFunction(() => { - formulas.forEach((formula) => parse(formula)); - }); - }); - - test('[CustomFormula] compute() batch processing 100 computations', async () => { - const formulas = Array.from({length: 100}, (_, i) => `{report:type} ${i} - {report:total} - {report:policyname}`); - - await measureFunction(() => { - formulas.forEach((formula) => compute(formula, mockContext)); - }); - }); - - test('[CustomFormula] end-to-end batch processing (parse + compute)', async () => { - const formulas = Array.from({length: 50}, (_, i) => `Expense ${i}: {report:type} - {report:startdate} - {report:total} {report:currency} for {report:policyname}`); - - await measureFunction(() => { - formulas.forEach((formula) => { - const parts = parse(formula); - const result = compute(formula, mockContext); - return {parts, result}; - }); - }); - }); - }); - - describe('Edge Cases Performance', () => { - test('[CustomFormula] large formula with many parts', async () => { - const largeParts = Array.from({length: 20}, (_, i) => `{report:field${i}}`).join(' - '); - const formula = `Large Formula: ${largeParts}`; - - await measureFunction(() => { - parse(formula); - compute(formula, mockContext); - }); - }); - - test('[CustomFormula] deeply nested functions', async () => { - const formula = '{user:email|frontPart} - {field:description|substr:0:20} - {report:created:yyyy-MM-dd}'; - - await measureFunction(() => { - parse(formula); - compute(formula, mockContext); - }); - }); - - test('[CustomFormula] formula with escaped braces', async () => { - const formula = '\\{not-formula} {report:type} \\{escaped} {report:total} \\{more-escapes}'; - - await measureFunction(() => { - parse(formula); - compute(formula, mockContext); - }); - }); - }); -}); diff --git a/tests/perf-test/OptimisticReportNames.perf-test.ts b/tests/perf-test/OptimisticReportNames.perf-test.ts deleted file mode 100644 index b6dcdbf87cfb9..0000000000000 --- a/tests/perf-test/OptimisticReportNames.perf-test.ts +++ /dev/null @@ -1,283 +0,0 @@ -import Onyx from 'react-native-onyx'; -import {measureFunction} from 'reassure'; -import type {UpdateContext} from '@libs/OptimisticReportNames'; -import {computeReportNameIfNeeded, updateOptimisticReportNamesFromUpdates} from '@libs/OptimisticReportNames'; -// eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportUtils to mock -import * as ReportUtils from '@libs/ReportUtils'; -import type {OnyxKey} from '@src/ONYXKEYS'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, Report} from '@src/types/onyx'; -import createCollection from '../utils/collections/createCollection'; -import {createRandomReport} from '../utils/collections/reports'; -import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; - -// Mock dependencies -jest.mock('@libs/ReportUtils', () => ({ - // jest.requireActual is necessary to include multi-layered module imports (eg. Report.ts has processReportIDDeeplink() which uses parseReportRouteParams() imported from getReportIDFromUrl.ts) - // Without jest.requireActual, parseReportRouteParams would be undefined, causing the test to fail. - ...jest.requireActual('@libs/ReportUtils'), - // These methods are mocked below in the beforeAll function to return specific values - isExpenseReport: jest.fn(), - getTitleReportField: jest.fn(), -})); -jest.mock('@libs/Performance', () => ({ - markStart: jest.fn(), - markEnd: jest.fn(), -})); -jest.mock('@libs/actions/Timing', () => ({ - start: jest.fn(), - end: jest.fn(), -})); -jest.mock('@libs/Log', () => ({ - info: jest.fn(), -})); - -const mockReportUtils = ReportUtils as jest.Mocked; - -describe('[OptimisticReportNames] Performance Tests', () => { - const REPORTS_COUNT = 1000; - const POLICIES_COUNT = 100; - - const mockPolicy = { - id: 'policy1', - name: 'Test Policy', - fieldList: { - // eslint-disable-next-line @typescript-eslint/naming-convention - text_title: { - defaultValue: '{report:type} - {report:startdate} - {report:total} {report:currency}', - }, - }, - } as unknown as Policy; - - const mockPolicies = createCollection( - (item) => `policy_${item.id}`, - (index) => ({ - ...mockPolicy, - id: `policy${index}`, - name: `Policy ${index}`, - }), - POLICIES_COUNT, - ); - - const mockReports = createCollection( - (item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, - (index) => ({ - ...createRandomReport(index), - policyID: `policy${index % POLICIES_COUNT}`, - total: -(Math.random() * 100000), // Random negative amount - currency: 'USD', - lastVisibleActionCreated: new Date().toISOString(), - }), - REPORTS_COUNT, - ); - - const mockContext: UpdateContext = { - betas: ['authAutoReportTitle'], - allReports: mockReports, - allPolicies: mockPolicies, - allReportNameValuePairs: {}, - allTransactions: {}, - }; - - beforeAll(async () => { - Onyx.init({keys: ONYXKEYS}); - mockReportUtils.isExpenseReport.mockReturnValue(true); - mockReportUtils.getTitleReportField.mockReturnValue(mockPolicy.fieldList?.text_title); - await waitForBatchedUpdates(); - }); - - afterAll(() => { - Onyx.clear(); - }); - - describe('Single Report Name Computation', () => { - test('[OptimisticReportNames] computeReportNameIfNeeded() single report', async () => { - const report = Object.values(mockReports).at(0); - const update = { - key: `report_${report?.reportID}` as OnyxKey, - onyxMethod: Onyx.METHOD.MERGE, - value: {total: -20000}, - }; - - await measureFunction(() => computeReportNameIfNeeded(report, update, mockContext)); - }); - }); - - describe('Batch Processing Performance', () => { - test('[OptimisticReportNames] updateOptimisticReportNamesFromUpdates() with 10 new reports', async () => { - const updates = Array.from({length: 10}, (_, i) => ({ - key: `report_new${i}` as OnyxKey, - onyxMethod: Onyx.METHOD.SET, - value: { - reportID: `new${i}`, - policyID: `policy${i % 10}`, - total: -(Math.random() * 50000), - currency: 'USD', - lastVisibleActionCreated: new Date().toISOString(), - }, - })); - - await measureFunction(() => updateOptimisticReportNamesFromUpdates(updates, mockContext)); - }); - - test('[OptimisticReportNames] updateOptimisticReportNamesFromUpdates() with 50 existing report updates', async () => { - const reportKeys = Object.keys(mockReports).slice(0, 50) as OnyxKey[]; - const updates = reportKeys.map((key) => ({ - key, - onyxMethod: Onyx.METHOD.MERGE, - value: {total: -(Math.random() * 100000)}, - })); - - await measureFunction(() => updateOptimisticReportNamesFromUpdates(updates, mockContext)); - }); - - test('[OptimisticReportNames] updateOptimisticReportNamesFromUpdates() with 100 mixed updates', async () => { - const newReportUpdates = Array.from({length: 50}, (_, i) => ({ - key: `report_batch${i}` as OnyxKey, - onyxMethod: Onyx.METHOD.SET, - value: { - reportID: `batch${i}`, - policyID: `policy${i % 20}`, - total: -(Math.random() * 75000), - currency: 'USD', - lastVisibleActionCreated: new Date().toISOString(), - }, - })); - - const existingReportUpdates = Object.keys(mockReports) - .slice(0, 50) - .map((key) => ({ - key: key as OnyxKey, - onyxMethod: Onyx.METHOD.MERGE, - value: {total: -(Math.random() * 125000)}, - })); - - const allUpdates = [...newReportUpdates, ...existingReportUpdates]; - - await measureFunction(() => updateOptimisticReportNamesFromUpdates(allUpdates, mockContext)); - }); - }); - - describe('Policy Update Impact Performance', () => { - test('[OptimisticReportNames] policy update affecting multiple reports', async () => { - const policyUpdate = { - key: 'policy_policy1' as OnyxKey, - onyxMethod: Onyx.METHOD.MERGE, - value: {name: 'Updated Policy Name'}, - }; - - // This should trigger name computation for all reports using policy1 - await measureFunction(() => updateOptimisticReportNamesFromUpdates([policyUpdate], mockContext)); - }); - - test('[OptimisticReportNames] multiple policy updates', async () => { - const policyUpdates = Array.from({length: 10}, (_, i) => ({ - key: `policy_policy${i}` as OnyxKey, - onyxMethod: Onyx.METHOD.MERGE, - value: {name: `Bulk Updated Policy ${i}`}, - })); - - await measureFunction(() => updateOptimisticReportNamesFromUpdates(policyUpdates, mockContext)); - }); - }); - - describe('Large Dataset Performance', () => { - test('[OptimisticReportNames] processing with large context (1000 reports)', async () => { - const updates = Array.from({length: 1000}, (_, i) => ({ - key: `report_large${i}` as OnyxKey, - onyxMethod: Onyx.METHOD.SET, - value: { - reportID: `large${i}`, - policyID: `policy${i % 50}`, - total: -(Math.random() * 200000), - currency: 'USD', - lastVisibleActionCreated: new Date().toISOString(), - }, - })); - - await measureFunction(() => updateOptimisticReportNamesFromUpdates(updates, mockContext)); - }); - - test('[OptimisticReportNames] worst case: many irrelevant updates', async () => { - // Create updates that won't trigger name computation to test filtering performance - const irrelevantUpdates = Array.from({length: 100}, (_, i) => ({ - key: `transaction_${i}` as OnyxKey, - onyxMethod: Onyx.METHOD.MERGE, - value: {description: `Updated transaction ${i}`}, - })); - - await measureFunction(() => updateOptimisticReportNamesFromUpdates(irrelevantUpdates, mockContext)); - }); - }); - - describe('Edge Cases Performance', () => { - test('[OptimisticReportNames] reports without formulas', async () => { - // Mock reports with policies that don't have formulas - const contextWithoutFormulas: UpdateContext = { - ...mockContext, - allPolicies: createCollection( - (item) => `policy_${item.id}`, - (index) => - ({ - id: `policy${index}`, - name: `Policy ${index}`, - fieldList: { - // eslint-disable-next-line @typescript-eslint/naming-convention - text_title: { - name: 'Title', - defaultValue: 'Static Title', - fieldID: 'text_title', - orderWeight: 0, - type: 'text' as const, - deletable: true, - values: [], - keys: [], - externalIDs: [], - disabledOptions: [], - isTax: false, - }, - }, - }) as unknown as Policy, - 50, - ), - allReportNameValuePairs: {}, - }; - - const updates = Array.from({length: 20}, (_, i) => ({ - key: `report_static${i}` as OnyxKey, - onyxMethod: Onyx.METHOD.SET, - value: { - reportID: `static${i}`, - policyID: `policy${i % 10}`, - total: -10000, - currency: 'USD', - }, - })); - - await measureFunction(() => updateOptimisticReportNamesFromUpdates(updates, contextWithoutFormulas)); - }); - - test('[OptimisticReportNames] missing policies and reports', async () => { - const contextWithMissingData: UpdateContext = { - betas: ['authAutoReportTitle'], - allReports: {}, - allPolicies: {}, - allReportNameValuePairs: {}, - allTransactions: {}, - }; - - const updates = Array.from({length: 10}, (_, i) => ({ - key: `report_missing${i}` as OnyxKey, - onyxMethod: Onyx.METHOD.SET, - value: { - reportID: `missing${i}`, - policyID: 'nonexistent', - total: -10000, - currency: 'USD', - }, - })); - - await measureFunction(() => updateOptimisticReportNamesFromUpdates(updates, contextWithMissingData)); - }); - }); -}); diff --git a/tests/unit/FormulaTest.ts b/tests/unit/FormulaTest.ts deleted file mode 100644 index 1298f0414b35d..0000000000000 --- a/tests/unit/FormulaTest.ts +++ /dev/null @@ -1,360 +0,0 @@ -// eslint-disable-next-line no-restricted-syntax -- disabled because we need CurrencyUtils to mock -import * as CurrencyUtils from '@libs/CurrencyUtils'; -import type {FormulaContext} from '@libs/Formula'; -import {compute, extract, parse} from '@libs/Formula'; -// eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportActionsUtils to mock -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; -// eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportUtils to mock -import * as ReportUtils from '@libs/ReportUtils'; -import CONST from '@src/CONST'; -import type {Policy, Report, ReportActions, Transaction} from '@src/types/onyx'; - -jest.mock('@libs/ReportActionsUtils', () => ({ - getAllReportActions: jest.fn(), -})); - -jest.mock('@libs/ReportUtils', () => ({ - ...jest.requireActual('@libs/ReportUtils'), - getReportTransactions: jest.fn(), -})); - -jest.mock('@libs/CurrencyUtils', () => ({ - getCurrencySymbol: jest.fn(), -})); - -const mockReportActionsUtils = ReportActionsUtils as jest.Mocked; -const mockReportUtils = ReportUtils as jest.Mocked; -const mockCurrencyUtils = CurrencyUtils as jest.Mocked; - -describe('CustomFormula', () => { - describe('extract()', () => { - test('should extract formula parts with default braces', () => { - expect(extract('{report:type} - {report:total}')).toEqual(['{report:type}', '{report:total}']); - }); - - test('should handle nested braces', () => { - expect(extract('{report:{report:submit:from:firstName|substr:2}}')).toEqual(['{report:{report:submit:from:firstName|substr:2}}']); - }); - - test('should handle escaped braces', () => { - expect(extract('\\{not-formula} {report:type}')).toEqual(['{report:type}']); - }); - - test('should handle empty formula', () => { - expect(extract('')).toEqual([]); - }); - - test('should handle formula without braces', () => { - expect(extract('no braces here')).toEqual([]); - }); - }); - - describe('parse()', () => { - test('should parse report formula parts', () => { - const parts = parse('{report:type} {report:startdate}'); - expect(parts).toHaveLength(3); - expect(parts.at(0)).toEqual({ - definition: '{report:type}', - type: 'report', - fieldPath: ['type'], - functions: [], - }); - expect(parts.at(2)).toEqual({ - definition: '{report:startdate}', - type: 'report', - fieldPath: ['startdate'], - functions: [], - }); - }); - - test('should parse field formula parts', () => { - const parts = parse('{field:custom_field}'); - expect(parts.at(0)).toEqual({ - definition: '{field:custom_field}', - type: 'field', - fieldPath: ['custom_field'], - functions: [], - }); - }); - - test('should parse user formula parts with functions', () => { - const parts = parse('{user:email|frontPart}'); - expect(parts.at(0)).toEqual({ - definition: '{user:email|frontPart}', - type: 'user', - fieldPath: ['email'], - functions: ['frontPart'], - }); - }); - - test('should handle empty formula', () => { - expect(parse('')).toEqual([]); - }); - - test('should treat formula without braces as free text', () => { - const parts = parse('no braces here'); - expect(parts).toHaveLength(1); - expect(parts.at(0)?.type).toBe('freetext'); - }); - }); - - describe('compute()', () => { - const mockContext: FormulaContext = { - report: { - reportID: '123', - reportName: '', - type: 'expense', - total: -10000, // -$100.00 - currency: 'USD', - lastVisibleActionCreated: '2025-01-15T10:30:00Z', - policyID: 'policy1', - } as Report, - policy: { - name: 'Test Policy', - } as Policy, - }; - - beforeEach(() => { - jest.clearAllMocks(); - - mockCurrencyUtils.getCurrencySymbol.mockImplementation((currency: string) => { - if (currency === 'USD') { - return '$'; - } - return currency; - }); - - const mockReportActions = { - // eslint-disable-next-line @typescript-eslint/naming-convention - '1': { - reportActionID: '1', - created: '2025-01-10T08:00:00Z', // Oldest action - actionName: 'CREATED', - }, - // eslint-disable-next-line @typescript-eslint/naming-convention - '2': { - reportActionID: '2', - created: '2025-01-15T10:30:00Z', // Later action - actionName: 'IOU', - }, - // eslint-disable-next-line @typescript-eslint/naming-convention - '3': { - reportActionID: '3', - created: '2025-01-12T14:20:00Z', // Middle action - actionName: 'COMMENT', - }, - } as unknown as ReportActions; - - const mockTransactions = [ - { - transactionID: 'trans1', - created: '2025-01-08T12:00:00Z', // Oldest transaction - amount: 5000, - merchant: 'ACME Ltd.', - }, - { - transactionID: 'trans2', - created: '2025-01-14T16:45:00Z', // Later transaction - amount: 3000, - merchant: 'ACME Ltd.', - }, - { - transactionID: 'trans3', - created: '2025-01-11T09:15:00Z', // Middle transaction - amount: 2000, - merchant: 'ACME Ltd.', - }, - ] as Transaction[]; - - mockReportActionsUtils.getAllReportActions.mockReturnValue(mockReportActions); - mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions); - }); - - test('should compute basic report formula', () => { - const result = compute('{report:type} {report:total}', mockContext); - expect(result).toBe('Expense Report $100.00'); // No space between parts - }); - - test('should compute startdate formula using transactions', () => { - const result = compute('{report:startdate}', mockContext); - expect(result).toBe('2025-01-08'); // Should use oldest transaction date (2025-01-08) - }); - - test('should compute created formula using report actions', () => { - const result = compute('{report:created}', mockContext); - expect(result).toBe('2025-01-10'); // Should use oldest report action date (2025-01-10) - }); - - test('should compute startdate with custom format', () => { - const result = compute('{report:startdate:MM/dd/yyyy}', mockContext); - expect(result).toBe('01/08/2025'); // Should use oldest transaction date with yyyy-MM-dd format - }); - - test('should compute created with custom format', () => { - const result = compute('{report:created:MMMM dd, yyyy}', mockContext); - expect(result).toBe('January 10, 2025'); // Should use oldest report action date with MMMM dd, yyyy format - }); - - test('should compute startdate with short month format', () => { - const result = compute('{report:startdate:dd MMM yyyy}', mockContext); - expect(result).toBe('08 Jan 2025'); // Should use oldest transaction date with dd MMM yyyy format - }); - - test('should compute policy name', () => { - const result = compute('{report:policyname}', mockContext); - expect(result).toBe('Test Policy'); - }); - - test('should handle empty formula', () => { - expect(compute('', mockContext)).toBe(''); - }); - - test('should handle unknown formula parts', () => { - const result = compute('{report:unknown}', mockContext); - expect(result).toBe('{report:unknown}'); - }); - - test('should handle missing report data gracefully', () => { - const contextWithMissingData: FormulaContext = { - report: {} as unknown as Report, - policy: null as unknown as Policy, - }; - const result = compute('{report:total} {report:policyname}', contextWithMissingData); - expect(result).toBe('{report:total} {report:policyname}'); // Empty data is replaced with definition - }); - - test('should preserve free text', () => { - const result = compute('Expense Report - {report:total}', mockContext); - expect(result).toBe('Expense Report - $100.00'); - }); - - test('should preserve exact spacing around formula parts', () => { - const result = compute('Report with type after 4 spaces {report:type}-and no space after computed part', mockContext); - expect(result).toBe('Report with type after 4 spaces Expense Report-and no space after computed part'); - }); - }); - - describe('Edge Cases', () => { - test('should handle malformed braces', () => { - const parts = parse('{incomplete'); - expect(parts.at(0)?.type).toBe('freetext'); - }); - - test('should handle undefined amounts', () => { - const context: FormulaContext = { - report: {total: undefined} as Report, - policy: null as unknown as Policy, - }; - const result = compute('{report:total}', context); - expect(result).toBe('{report:total}'); - }); - - test('should handle missing report actions for created', () => { - mockReportActionsUtils.getAllReportActions.mockReturnValue({}); - const context: FormulaContext = { - report: {reportID: '123'} as Report, - policy: null as unknown as Policy, - }; - - const result = compute('{report:created}', context); - expect(result).toBe('{report:created}'); - }); - - test('should handle missing transactions for startdate', () => { - mockReportUtils.getReportTransactions.mockReturnValue([]); - const context: FormulaContext = { - report: {reportID: '123'} as Report, - policy: null as unknown as Policy, - }; - const today = new Date(); - const expected = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; - const result = compute('{report:startdate}', context); - expect(result).toBe(expected); - }); - - test('should call getReportTransactions with correct reportID for startdate', () => { - const context: FormulaContext = { - report: {reportID: 'test-report-123'} as Report, - policy: null as unknown as Policy, - }; - - compute('{report:startdate}', context); - expect(mockReportUtils.getReportTransactions).toHaveBeenCalledWith('test-report-123'); - }); - - test('should call getAllReportActions with correct reportID for created', () => { - const context: FormulaContext = { - report: {reportID: 'test-report-456'} as Report, - policy: null as unknown as Policy, - }; - - compute('{report:created}', context); - expect(mockReportActionsUtils.getAllReportActions).toHaveBeenCalledWith('test-report-456'); - }); - - test('should skip partial transactions (empty merchant)', () => { - const mockTransactions = [ - { - transactionID: 'trans1', - created: '2025-01-15T12:00:00Z', - amount: 5000, - merchant: 'ACME Ltd.', - }, - { - transactionID: 'trans2', - created: '2025-01-08T16:45:00Z', // Older but partial - amount: 3000, - merchant: '', // Empty merchant = partial - }, - { - transactionID: 'trans3', - created: '2025-01-12T09:15:00Z', // Should be oldest valid - amount: 2000, - merchant: 'Gamma Inc.', - }, - ] as Transaction[]; - - mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions); - const context: FormulaContext = { - report: {reportID: 'test-report-123'} as Report, - policy: null as unknown as Policy, - }; - - const result = compute('{report:startdate}', context); - expect(result).toBe('2025-01-12'); - }); - - test('should skip partial transactions (zero amount)', () => { - const mockTransactions = [ - { - transactionID: 'trans1', - created: '2025-01-15T12:00:00Z', - amount: 5000, - merchant: 'ACME Ltd.', - }, - { - transactionID: 'trans2', - created: '2025-01-08T16:45:00Z', // Older but partial - amount: 0, // Zero amount = partial - merchant: 'Beta Corp.', - iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN, - }, - { - transactionID: 'trans3', - created: '2025-01-12T09:15:00Z', // Should be oldest valid - amount: 2000, - merchant: 'Gamma Inc.', - }, - ] as Transaction[]; - - mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions); - const context: FormulaContext = { - report: {reportID: 'test-report-123'} as Report, - policy: null as unknown as Policy, - }; - - const result = compute('{report:startdate}', context); - expect(result).toBe('2025-01-12'); - }); - }); -}); diff --git a/tests/unit/OptimisticReportNamesTest.ts b/tests/unit/OptimisticReportNamesTest.ts deleted file mode 100644 index 824f26394b3c1..0000000000000 --- a/tests/unit/OptimisticReportNamesTest.ts +++ /dev/null @@ -1,433 +0,0 @@ -import Onyx from 'react-native-onyx'; -import type {UpdateContext} from '@libs/OptimisticReportNames'; -import {computeReportNameIfNeeded, getReportByTransactionID, shouldComputeReportName, updateOptimisticReportNamesFromUpdates} from '@libs/OptimisticReportNames'; -// eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportUtils to mock -import * as ReportUtils from '@libs/ReportUtils'; -import type {OnyxKey} from '@src/ONYXKEYS'; -import type {Policy, PolicyReportField, Report, Transaction} from '@src/types/onyx'; - -// Mock dependencies -jest.mock('@libs/ReportUtils', () => ({ - ...jest.requireActual('@libs/ReportUtils'), - isExpenseReport: jest.fn(), - getTitleReportField: jest.fn(), - getReportTransactions: jest.fn(), -})); - -jest.mock('@libs/CurrencyUtils', () => ({ - getCurrencySymbol: jest.fn().mockReturnValue('$'), -})); - -const mockReportUtils = ReportUtils as jest.Mocked; - -describe('OptimisticReportNames', () => { - const mockPolicy = { - id: 'policy1', - fieldList: { - // eslint-disable-next-line @typescript-eslint/naming-convention - text_title: { - defaultValue: '{report:type} - {report:total}', - }, - }, - } as unknown as Policy; - - const mockReport = { - reportID: '123', - reportName: 'Old Name', - policyID: 'policy1', - total: -10000, - currency: 'USD', - lastVisibleActionCreated: '2025-01-15T10:30:00Z', - type: 'expense', - } as Report; - - const mockContext: UpdateContext = { - betas: ['authAutoReportTitle'], - allReports: { - // eslint-disable-next-line @typescript-eslint/naming-convention - report_123: mockReport, - }, - allPolicies: { - // eslint-disable-next-line @typescript-eslint/naming-convention - policy_policy1: mockPolicy, - }, - allReportNameValuePairs: { - // eslint-disable-next-line @typescript-eslint/naming-convention - reportNameValuePairs_123: { - private_isArchived: '', - }, - }, - allTransactions: {}, - }; - - beforeEach(() => { - jest.clearAllMocks(); - mockReportUtils.isExpenseReport.mockReturnValue(true); - mockReportUtils.getTitleReportField.mockReturnValue(mockPolicy.fieldList?.text_title); - }); - - describe('shouldComputeReportName()', () => { - test('should return true for expense report with title field formula', () => { - const result = shouldComputeReportName(mockReport, mockPolicy); - expect(result).toBe(true); - }); - - test('should return false for reports with unsupported type', () => { - mockReportUtils.isExpenseReport.mockReturnValue(false); - - const result = shouldComputeReportName( - { - ...mockReport, - type: 'iou', - } as Report, - mockPolicy, - ); - expect(result).toBe(false); - }); - - test('should return false when no policy', () => { - const result = shouldComputeReportName(mockReport, undefined); - expect(result).toBe(false); - }); - - test('should return false when no title field', () => { - mockReportUtils.getTitleReportField.mockReturnValue(undefined); - const result = shouldComputeReportName(mockReport, mockPolicy); - expect(result).toBe(false); - }); - - test('should return true when title field has no formula', () => { - const policyWithoutFormula = { - ...mockPolicy, - fieldList: { - // eslint-disable-next-line @typescript-eslint/naming-convention - text_title: {defaultValue: 'Static Title'}, - }, - } as unknown as Policy; - mockReportUtils.getTitleReportField.mockReturnValue(policyWithoutFormula.fieldList?.text_title); - const result = shouldComputeReportName(mockReport, policyWithoutFormula); - expect(result).toBe(true); - }); - }); - - describe('computeReportNameIfNeeded()', () => { - test('should compute name when report data changes', () => { - const update = { - key: 'report_123' as OnyxKey, - onyxMethod: Onyx.METHOD.MERGE, - value: {total: -20000}, - }; - - const result = computeReportNameIfNeeded(mockReport, update, mockContext); - expect(result).toEqual('Expense Report - $200.00'); - }); - - test('should return null when name would not change', () => { - const update = { - key: 'report_456' as OnyxKey, - onyxMethod: Onyx.METHOD.MERGE, - value: {description: 'Updated description'}, - }; - - const result = computeReportNameIfNeeded( - { - ...mockReport, - reportName: 'Expense Report - $100.00', - }, - update, - mockContext, - ); - expect(result).toBeNull(); - }); - }); - - describe('updateOptimisticReportNamesFromUpdates()', () => { - test('should detect new report creation and add name update', () => { - const updates = [ - { - key: 'report_456' as OnyxKey, - onyxMethod: Onyx.METHOD.SET, - value: { - reportID: '456', - policyID: 'policy1', - total: -15000, - currency: 'USD', - type: 'expense', - }, - }, - ]; - - const result = updateOptimisticReportNamesFromUpdates(updates, mockContext); - expect(result).toHaveLength(2); // Original + name update - expect(result.at(1)).toEqual({ - key: 'report_456', - onyxMethod: Onyx.METHOD.MERGE, - value: {reportName: 'Expense Report - $150.00'}, - }); - }); - - test('should handle existing report updates', () => { - const updates = [ - { - key: 'report_123' as OnyxKey, - onyxMethod: Onyx.METHOD.MERGE, - value: {total: -25000}, - }, - ]; - - const result = updateOptimisticReportNamesFromUpdates(updates, mockContext); - expect(result).toHaveLength(2); // Original + name update - expect(result.at(1)?.value).toEqual({reportName: 'Expense Report - $250.00'}); - }); - - test('should handle policy updates affecting multiple reports', () => { - const contextWithMultipleReports = { - ...mockContext, - allReports: { - // eslint-disable-next-line @typescript-eslint/naming-convention - report_123: {...mockReport, reportID: '123'}, - // eslint-disable-next-line @typescript-eslint/naming-convention - report_456: {...mockReport, reportID: '456'}, - // eslint-disable-next-line @typescript-eslint/naming-convention - report_789: {...mockReport, reportID: '789'}, - }, - allReportNameValuePairs: { - // eslint-disable-next-line @typescript-eslint/naming-convention - reportNameValuePairs_123: {private_isArchived: ''}, - // eslint-disable-next-line @typescript-eslint/naming-convention - reportNameValuePairs_456: {private_isArchived: ''}, - // eslint-disable-next-line @typescript-eslint/naming-convention - reportNameValuePairs_789: {private_isArchived: ''}, - }, - }; - mockReportUtils.getTitleReportField.mockReturnValue({defaultValue: 'Policy: {report:policyname}'} as unknown as PolicyReportField); - - const updates = [ - { - key: 'policy_policy1' as OnyxKey, - onyxMethod: Onyx.METHOD.MERGE, - value: {name: 'Updated Policy Name'}, - }, - ]; - - const result = updateOptimisticReportNamesFromUpdates(updates, contextWithMultipleReports); - - expect(result).toHaveLength(4); - - // Assert the original policy update - expect(result.at(0)).toEqual({ - key: 'policy_policy1', - onyxMethod: Onyx.METHOD.MERGE, - value: {name: 'Updated Policy Name'}, - }); - - // Assert individual report name updates - expect(result.at(1)).toEqual({ - key: 'report_123', - onyxMethod: Onyx.METHOD.MERGE, - value: {reportName: 'Policy: Updated Policy Name'}, - }); - - expect(result.at(2)).toEqual({ - key: 'report_456', - onyxMethod: Onyx.METHOD.MERGE, - value: {reportName: 'Policy: Updated Policy Name'}, - }); - - expect(result.at(3)).toEqual({ - key: 'report_789', - onyxMethod: Onyx.METHOD.MERGE, - value: {reportName: 'Policy: Updated Policy Name'}, - }); - }); - - test('should handle unknown object types gracefully', () => { - const updates = [ - { - key: 'unknown_123' as OnyxKey, - onyxMethod: Onyx.METHOD.MERGE, - value: {someData: 'value'}, - }, - ]; - - const result = updateOptimisticReportNamesFromUpdates(updates, mockContext); - expect(result).toEqual(updates); // Unchanged - }); - }); - - describe('Edge Cases', () => { - test('should handle missing report gracefully', () => { - const update = { - key: 'report_999' as OnyxKey, - onyxMethod: Onyx.METHOD.MERGE, - value: {total: -10000}, - }; - - const result = computeReportNameIfNeeded(undefined, update, mockContext); - expect(result).toBeNull(); - }); - }); - - describe('Transaction Updates', () => { - test('should process transaction updates and trigger report name updates', () => { - const contextWithTransaction = { - ...mockContext, - allTransactions: { - // eslint-disable-next-line @typescript-eslint/naming-convention - transactions_txn123: { - transactionID: 'txn123', - reportID: '123', - created: '2024-01-01', - amount: -5000, - currency: 'USD', - merchant: 'Original Merchant', - }, - }, - }; - - const update = { - key: 'transactions_txn123' as OnyxKey, - onyxMethod: Onyx.METHOD.MERGE, - value: { - created: '2024-02-15', // Updated date - reportID: '123', - }, - }; - - const result = updateOptimisticReportNamesFromUpdates([update], contextWithTransaction); - - // Should include original update + new report name update - expect(result).toHaveLength(2); - expect(result.at(0)).toEqual(update); // Original transaction update - expect(result.at(1)?.key).toBe('report_123'); // New report update - }); - - test('getReportByTransactionID should find report from transaction', () => { - const contextWithTransaction = { - ...mockContext, - allTransactions: { - // eslint-disable-next-line @typescript-eslint/naming-convention - transactions_abc123: { - transactionID: 'abc123', - reportID: '123', - amount: -7500, - created: '2024-01-15', - currency: 'USD', - merchant: 'Test Store', - }, - }, - }; - - const result = getReportByTransactionID('abc123', contextWithTransaction); - - expect(result).toEqual(mockReport); - expect(result?.reportID).toBe('123'); - }); - - test('getReportByTransactionID should return undefined for missing transaction', () => { - const result = getReportByTransactionID('nonexistent', mockContext); - expect(result).toBeUndefined(); - }); - - test('getReportByTransactionID should return undefined for transaction without reportID', () => { - const contextWithIncompleteTransaction = { - ...mockContext, - allTransactions: { - // eslint-disable-next-line @typescript-eslint/naming-convention - transactions_incomplete: { - transactionID: 'incomplete' as OnyxKey, - amount: -1000, - currency: 'USD', - merchant: 'Store', - // Missing reportID - } as unknown as Transaction, - }, - }; - - const result = getReportByTransactionID('incomplete', contextWithIncompleteTransaction); - expect(result).toBeUndefined(); - }); - - test('should handle transaction updates that rely on context lookup', () => { - const contextWithTransaction = { - ...mockContext, - allTransactions: { - // eslint-disable-next-line @typescript-eslint/naming-convention - transactions_xyz789: { - transactionID: 'xyz789', - reportID: '123', - created: '2024-01-01', - amount: -3000, - currency: 'EUR', - merchant: 'Context Store', - }, - }, - }; - - // Transaction update without reportID in the value - const update = { - key: 'transactions_xyz789' as OnyxKey, - onyxMethod: Onyx.METHOD.MERGE, - value: { - amount: -4000, // Updated amount - // No reportID provided in update - }, - }; - - const result = updateOptimisticReportNamesFromUpdates([update], contextWithTransaction); - - // Should still find the report through context lookup and generate update - expect(result).toHaveLength(2); - expect(result.at(1)?.key).toBe('report_123'); - }); - - test('should use optimistic transaction data in formula computation', () => { - mockReportUtils.getTitleReportField.mockReturnValue({ - defaultValue: 'Report from {report:startdate}', - } as unknown as PolicyReportField); - - const contextWithTransaction = { - ...mockContext, - allTransactions: { - // eslint-disable-next-line @typescript-eslint/naming-convention - transactions_formula123: { - transactionID: 'formula123', - reportID: '123', - created: '2024-01-01', // Original date - amount: -5000, - currency: 'USD', - merchant: 'Original Store', - }, - }, - }; - - // Mock getReportTransactions to return the original transaction - // eslint-disable-next-line @typescript-eslint/dot-notation - mockReportUtils.getReportTransactions.mockReturnValue([contextWithTransaction.allTransactions['transactions_formula123']]); - - const update = { - key: 'transactions_formula123' as OnyxKey, - onyxMethod: Onyx.METHOD.MERGE, - value: { - transactionID: 'formula123', - created: '2024-03-15', // Updated date that should be used in formula - modifiedCreated: '2024-03-15', - }, - }; - - const result = updateOptimisticReportNamesFromUpdates([update], contextWithTransaction); - - expect(result).toHaveLength(2); - - // The key test: verify exact report name with optimistic date - const reportUpdate = result.at(1); - expect(reportUpdate).toEqual({ - key: 'report_123', - onyxMethod: Onyx.METHOD.MERGE, - value: { - reportName: 'Report from 2024-03-15', // Exact expected result with updated date - }, - }); - }); - }); -}); From f32944151e32f0064aed735354b772bd6b876ddd Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Fri, 22 Aug 2025 18:09:23 +0800 Subject: [PATCH 2/2] Update src/libs/Permissions.ts Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com> --- src/libs/Permissions.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 979c0df7e252b..408df3326275e 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -33,5 +33,4 @@ export default { canUseLinkPreviews, isBlockedFromSpotnanaTravel, isBetaEnabled, - canUseAllBetas, };