From cf60401118d5e9f17f78fbb6a27cf83c36241405 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Fri, 30 Jan 2026 14:13:41 -0800 Subject: [PATCH 1/5] Seed Spend over time suggested search --- src/CONST/index.ts | 2 ++ src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/SearchUIUtils.ts | 38 +++++++++++++++++++++++++++++++++++++- 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 55eaf64d919f1..5af548dbc4a94 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -7182,6 +7182,7 @@ const CONST = { VIEW: { TABLE: 'table', BAR: 'bar', + LINE: 'line', }, SYNTAX_FILTER_KEYS: { TYPE: 'type', @@ -7398,6 +7399,7 @@ const CONST = { TOP_SPENDERS: 'topSpenders', TOP_CATEGORIES: 'topCategories', TOP_MERCHANTS: 'topMerchants', + SPEND_OVER_TIME: 'spendOverTime', }, GROUP_PREFIX: 'group_', ANIMATION: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 78fe93d1e5d07..bbc96bffedbd5 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -6913,6 +6913,7 @@ const translations = { savedSearchesMenuItemTitle: 'Saved', topCategories: 'Top categories', topMerchants: 'Top merchants', + spendOverTime: 'Spend over time', groupedExpenses: 'grouped expenses', bulkActions: { approve: 'Approve', diff --git a/src/languages/es.ts b/src/languages/es.ts index 3a3e63299da42..8d033e603be6b 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6664,6 +6664,7 @@ ${amount} para ${merchant} - ${date}`, savedSearchesMenuItemTitle: 'Guardadas', topCategories: 'Categorías principales', topMerchants: 'Principales comerciantes', + spendOverTime: 'Gastos a lo largo del tiempo', searchName: 'Nombre de la búsqueda', deleteSavedSearch: 'Eliminar búsqueda guardada', deleteSavedSearchConfirm: '¿Estás seguro de que quieres eliminar esta búsqueda?', diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 287e33075044b..38394eee8a79a 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -752,6 +752,33 @@ function getSuggestedSearches( CONST.SEARCH.GROUP_BY.MERCHANT, CONST.SEARCH.TOP_SEARCH_LIMIT, ), + [CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]: { + key: CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME, + translationPath: 'search.spendOverTime', + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + icon: 'Receipt', + searchQuery: buildQueryStringFromFilterFormValues( + { + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + groupBy: CONST.SEARCH.GROUP_BY.MONTH, + dateOn: CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE, + view: CONST.SEARCH.VIEW.BAR, + }, + { + sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH, + sortOrder: CONST.SEARCH.SORT_ORDER.DESC, + }, + ), + get searchQueryJSON() { + return buildSearchQueryJSON(this.searchQuery); + }, + get hash() { + return this.searchQueryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID; + }, + get similarSearchHash() { + return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID; + }, + }, }; } @@ -776,6 +803,7 @@ function getSuggestedSearchesVisibility( let shouldShowTopSpendersSuggestion = false; let shouldShowTopCategoriesSuggestion = false; let shouldShowTopMerchantsSuggestion = false; + let shouldShowSpendOverTimeSuggestion = false; const hasCardFeed = Object.values(cardFeedsByPolicy ?? {}).some((feeds) => feeds.length > 0); @@ -814,6 +842,7 @@ function getSuggestedSearchesVisibility( const isEligibleForTopSpendersSuggestion = isPaidPolicy && (isAdmin || isAuditor || isApprover); const isEligibleForTopCategoriesSuggestion = isPaidPolicy && policy.areCategoriesEnabled === true; const isEligibleForTopMerchantsSuggestion = isPaidPolicy; + const isEligibleForSpendOverTimeSuggestion = isPaidPolicy && (isAdmin || isAuditor || isApprover); shouldShowSubmitSuggestion ||= isEligibleForSubmitSuggestion; shouldShowPaySuggestion ||= isEligibleForPaySuggestion; @@ -826,6 +855,7 @@ function getSuggestedSearchesVisibility( shouldShowTopSpendersSuggestion ||= isEligibleForTopSpendersSuggestion; shouldShowTopCategoriesSuggestion ||= isEligibleForTopCategoriesSuggestion; shouldShowTopMerchantsSuggestion ||= isEligibleForTopMerchantsSuggestion; + shouldShowSpendOverTimeSuggestion ||= isEligibleForSpendOverTimeSuggestion; // We don't need to check the rest of the policies if we already determined that all suggestions should be displayed return ( @@ -858,6 +888,7 @@ function getSuggestedSearchesVisibility( [CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS]: shouldShowTopSpendersSuggestion, [CONST.SEARCH.SEARCH_KEYS.TOP_CATEGORIES]: shouldShowTopCategoriesSuggestion, [CONST.SEARCH.SEARCH_KEYS.TOP_MERCHANTS]: shouldShowTopMerchantsSuggestion, + [CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]: shouldShowSpendOverTimeSuggestion, }; } @@ -3626,7 +3657,12 @@ function createTypeMenuSections( menuItems: [], }; - const insightsSearchKeys = [CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS, CONST.SEARCH.SEARCH_KEYS.TOP_CATEGORIES, CONST.SEARCH.SEARCH_KEYS.TOP_MERCHANTS]; + const insightsSearchKeys = [ + CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS, + CONST.SEARCH.SEARCH_KEYS.TOP_CATEGORIES, + CONST.SEARCH.SEARCH_KEYS.TOP_MERCHANTS, + CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME, + ]; for (const key of insightsSearchKeys) { if (!suggestedSearchesVisibility[key]) { From 3903c5505935d63687ae7b61b97528765a88fc1c Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Fri, 30 Jan 2026 14:37:18 -0800 Subject: [PATCH 2/5] Add translations --- src/languages/de.ts | 3 ++- src/languages/en.ts | 1 + src/languages/fr.ts | 3 ++- src/languages/it.ts | 3 ++- src/languages/ja.ts | 3 ++- src/languages/nl.ts | 3 ++- src/languages/pl.ts | 3 ++- src/languages/pt-BR.ts | 3 ++- src/languages/zh-hans.ts | 3 ++- 9 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index a5904b5ce028e..a2c9fb43c23c5 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7090,7 +7090,7 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard allMatchingItemsSelected: 'Alle passenden Elemente ausgewählt', }, topSpenders: 'Top-Ausgaben', - view: {label: 'Ansehen', table: 'Tabelle', bar: 'Bar'}, + view: {label: 'Ansehen', table: 'Tabelle', bar: 'Bar', line: 'Zeile'}, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: 'Von', [CONST.SEARCH.GROUP_BY.CARD]: 'Karten', @@ -7103,6 +7103,7 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard [CONST.SEARCH.GROUP_BY.YEAR]: 'Jahre', [CONST.SEARCH.GROUP_BY.QUARTER]: 'Quartale', }, + spendOverTime: 'Ausgaben im Zeitverlauf', }, genericErrorPage: { title: 'Oh je, etwas ist schiefgelaufen!', diff --git a/src/languages/en.ts b/src/languages/en.ts index bbc96bffedbd5..8ed142349ac46 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7003,6 +7003,7 @@ const translations = { label: 'View', table: 'Table', bar: 'Bar', + line: 'Line', }, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: 'From', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 55297041b7030..06da9a5e53faa 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7102,7 +7102,7 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin allMatchingItemsSelected: 'Tous les éléments correspondants sont sélectionnés', }, topSpenders: 'Plus gros dépensiers', - view: {label: 'Afficher', table: 'Tableau', bar: 'Barre'}, + view: {label: 'Afficher', table: 'Tableau', bar: 'Barre', line: 'Ligne'}, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: 'De', [CONST.SEARCH.GROUP_BY.CARD]: 'Cartes', @@ -7115,6 +7115,7 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin [CONST.SEARCH.GROUP_BY.YEAR]: 'Années', [CONST.SEARCH.GROUP_BY.QUARTER]: 'Trimestres', }, + spendOverTime: 'Dépenses au fil du temps', }, genericErrorPage: { title: 'Oh oh, quelque chose s’est mal passé !', diff --git a/src/languages/it.ts b/src/languages/it.ts index 169e7064b93c1..d932590600a89 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7079,7 +7079,7 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori allMatchingItemsSelected: 'Tutti gli elementi corrispondenti selezionati', }, topSpenders: 'Maggiori spenditori', - view: {label: 'Visualizza', table: 'Tabella', bar: 'Bar'}, + view: {label: 'Visualizza', table: 'Tabella', bar: 'Bar', line: 'Riga'}, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: 'Da', [CONST.SEARCH.GROUP_BY.CARD]: 'Carte', @@ -7092,6 +7092,7 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori [CONST.SEARCH.GROUP_BY.YEAR]: 'Anni', [CONST.SEARCH.GROUP_BY.QUARTER]: 'Trimestri', }, + spendOverTime: 'Spesa nel tempo', }, genericErrorPage: { title: 'Uh-oh, qualcosa è andato storto!', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index c4669e8b00932..5d1be06c4e3e9 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7017,7 +7017,7 @@ ${reportName} allMatchingItemsSelected: '一致する項目をすべて選択済み', }, topSpenders: 'トップ支出者', - view: {label: '表示', table: 'テーブル', bar: 'バー'}, + view: {label: '表示', table: 'テーブル', bar: 'バー', line: '行'}, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: '差出人', [CONST.SEARCH.GROUP_BY.CARD]: 'カード', @@ -7030,6 +7030,7 @@ ${reportName} [CONST.SEARCH.GROUP_BY.YEAR]: '年', [CONST.SEARCH.GROUP_BY.QUARTER]: '四半期', }, + spendOverTime: '時間別支出', }, genericErrorPage: { title: 'おっと、問題が発生しました!', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index e6bd121bd966e..e5f91fad3c6f9 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7062,7 +7062,7 @@ Vraag verplichte uitgavedetails zoals bonnetjes en beschrijvingen, stel limieten allMatchingItemsSelected: 'Alle overeenkomende items geselecteerd', }, topSpenders: 'Grootste uitgaven', - view: {label: 'Bekijken', table: 'Tabel', bar: 'Bar'}, + view: {label: 'Bekijken', table: 'Tabel', bar: 'Bar', line: 'Regel'}, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: 'Van', [CONST.SEARCH.GROUP_BY.CARD]: 'Kaarten', @@ -7075,6 +7075,7 @@ Vraag verplichte uitgavedetails zoals bonnetjes en beschrijvingen, stel limieten [CONST.SEARCH.GROUP_BY.YEAR]: 'Jaren', [CONST.SEARCH.GROUP_BY.QUARTER]: 'Kwartalen', }, + spendOverTime: 'Uitgaven in de tijd', }, genericErrorPage: { title: 'O jee, er is iets misgegaan!', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 8953295506bae..4c5420e9737a7 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7049,7 +7049,7 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i allMatchingItemsSelected: 'Wybrano wszystkie pasujące elementy', }, topSpenders: 'Najwięksi wydający', - view: {label: 'Zobacz', table: 'Tabela', bar: 'Pasek'}, + view: {label: 'Zobacz', table: 'Tabela', bar: 'Pasek', line: 'Linia'}, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: 'Od', [CONST.SEARCH.GROUP_BY.CARD]: 'Karty', @@ -7062,6 +7062,7 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i [CONST.SEARCH.GROUP_BY.YEAR]: 'Lata', [CONST.SEARCH.GROUP_BY.QUARTER]: 'Kwartały', }, + spendOverTime: 'Wydatki w czasie', }, genericErrorPage: { title: 'Ups, coś poszło nie tak!', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 74bc594d78d7e..ba717f28d7057 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7051,7 +7051,7 @@ Exija detalhes de despesas como recibos e descrições, defina limites e padrõe allMatchingItemsSelected: 'Todos os itens correspondentes selecionados', }, topSpenders: 'Maiores gastadores', - view: {label: 'Ver', table: 'Tabela', bar: 'Bar'}, + view: {label: 'Ver', table: 'Tabela', bar: 'Bar', line: 'Linha'}, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: 'De', [CONST.SEARCH.GROUP_BY.CARD]: 'Cartões', @@ -7064,6 +7064,7 @@ Exija detalhes de despesas como recibos e descrições, defina limites e padrõe [CONST.SEARCH.GROUP_BY.YEAR]: 'Anos', [CONST.SEARCH.GROUP_BY.QUARTER]: 'Trimestres', }, + spendOverTime: 'Gastos ao longo do tempo', }, genericErrorPage: { title: 'Opa, algo deu errado!', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 41c860b495bc3..8608289985c7a 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -6896,7 +6896,7 @@ ${reportName} allMatchingItemsSelected: '已选择所有匹配的项目', }, topSpenders: '最高支出者', - view: {label: '查看', table: '表格', bar: '栏'}, + view: {label: '查看', table: '表格', bar: '栏', line: '行'}, chartTitles: { [CONST.SEARCH.GROUP_BY.FROM]: '来自', [CONST.SEARCH.GROUP_BY.CARD]: '卡片', @@ -6909,6 +6909,7 @@ ${reportName} [CONST.SEARCH.GROUP_BY.YEAR]: '年', [CONST.SEARCH.GROUP_BY.QUARTER]: '季度', }, + spendOverTime: '一段时间内的支出', }, genericErrorPage: { title: '哎呀,出错了!', From 1a464e174218a2ca42f901797a80fdef0013a0a5 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Fri, 30 Jan 2026 14:43:38 -0800 Subject: [PATCH 3/5] Add Spanish translation --- src/languages/es.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 8d033e603be6b..75694ac0bef2d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6659,12 +6659,12 @@ ${amount} para ${merchant} - ${date}`, unapprovedCard: 'Tarjeta no aprobada', reconciliation: 'Conciliación', topSpenders: 'Mayores gastadores', - view: {label: 'Ver', table: 'Tabla', bar: 'Barra'}, + view: {label: 'Ver', table: 'Tabla', bar: 'Barra', line: 'Línea'}, saveSearch: 'Guardar búsqueda', savedSearchesMenuItemTitle: 'Guardadas', topCategories: 'Categorías principales', topMerchants: 'Principales comerciantes', - spendOverTime: 'Gastos a lo largo del tiempo', + spendOverTime: 'Evolución de gastos', searchName: 'Nombre de la búsqueda', deleteSavedSearch: 'Eliminar búsqueda guardada', deleteSavedSearchConfirm: '¿Estás seguro de que quieres eliminar esta búsqueda?', From c9e846651a4fd1fa50fabf10c871e45bef5dec7d Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Fri, 30 Jan 2026 14:57:33 -0800 Subject: [PATCH 4/5] Add tests --- tests/unit/Search/SpendOverTimeTest.ts | 180 +++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 tests/unit/Search/SpendOverTimeTest.ts diff --git a/tests/unit/Search/SpendOverTimeTest.ts b/tests/unit/Search/SpendOverTimeTest.ts new file mode 100644 index 0000000000000..254c90448d372 --- /dev/null +++ b/tests/unit/Search/SpendOverTimeTest.ts @@ -0,0 +1,180 @@ +import type {OnyxCollection} from 'react-native-onyx'; +import CONST from '@src/CONST'; +import * as SearchUIUtils from '@src/libs/SearchUIUtils'; +import type * as OnyxTypes from '@src/types/onyx'; + +const adminAccountID = 18439984; +const adminEmail = 'admin@policy.com'; +const auditorEmail = 'auditor@policy.com'; +const approverEmail = 'approver@policy.com'; +const userEmail = 'user@policy.com'; +const policyID = 'A1B2C3'; + +describe('Test Spend Over Time Search', () => { + + describe('Test getSuggestedSearchesVisibility for Spend Over Time', () => { + test('Should show Spend Over Time for Admin role in paid policy', () => { + const policyKey = `policy_${policyID}`; + + const policies: OnyxCollection = { + [policyKey]: { + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.ADMIN, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined); + expect(response.spendOverTime).toBe(true); + }); + + test('Should show Spend Over Time for Auditor role in paid policy', () => { + const policyKey = `policy_${policyID}`; + + const policies: OnyxCollection = { + [policyKey]: { + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.AUDITOR, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(auditorEmail, {}, policies, undefined); + expect(response.spendOverTime).toBe(true); + }); + + test('Should show Spend Over Time for Approver role in paid policy', () => { + const policyKey = `policy_${policyID}`; + + const policies: OnyxCollection = { + [policyKey]: { + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + approver: approverEmail, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(approverEmail, {}, policies, undefined); + expect(response.spendOverTime).toBe(true); + }); + + test('Should hide Spend Over Time for User role in paid policy', () => { + const policyKey = `policy_${policyID}`; + + const policies: OnyxCollection = { + [policyKey]: { + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.USER, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(userEmail, {}, policies, undefined); + expect(response.spendOverTime).toBe(false); + }); + + test('Should hide Spend Over Time for free policies even with Admin role', () => { + const policyKey = `policy_${policyID}`; + + const policies: OnyxCollection = { + [policyKey]: { + id: policyID, + type: CONST.POLICY.TYPE.PERSONAL, + role: CONST.POLICY.ROLE.ADMIN, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined); + expect(response.spendOverTime).toBe(false); + }); + + test('Should show Spend Over Time if at least one policy has Admin/Auditor/Approver role', () => { + const policies: OnyxCollection = { + policyOne: { + id: 'policyOne', + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.USER, + } as OnyxTypes.Policy, + policyTwo: { + id: 'policyTwo', + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.ADMIN, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined); + expect(response.spendOverTime).toBe(true); + }); + + test('Should hide Spend Over Time if all policies have User role', () => { + const policies: OnyxCollection = { + policyOne: { + id: 'policyOne', + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.USER, + } as OnyxTypes.Policy, + policyTwo: { + id: 'policyTwo', + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.USER, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(userEmail, {}, policies, undefined); + expect(response.spendOverTime).toBe(false); + }); + }); + + describe('Test getSuggestedSearches for Spend Over Time', () => { + test('Should return Spend Over Time search with correct properties', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined); + const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]; + + expect(spendOverTimeSearch).toBeDefined(); + expect(spendOverTimeSearch.key).toBe(CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME); + expect(spendOverTimeSearch.translationPath).toBe('search.spendOverTime'); + expect(spendOverTimeSearch.type).toBe(CONST.SEARCH.DATA_TYPES.EXPENSE); + expect(spendOverTimeSearch.icon).toBe('Receipt'); + }); + + test('Should return Spend Over Time search query with correct parameters', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined); + const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]; + const searchQueryJSON = spendOverTimeSearch.searchQueryJSON; + + expect(searchQueryJSON).toBeDefined(); + expect(searchQueryJSON?.type).toBe(CONST.SEARCH.DATA_TYPES.EXPENSE); + expect(searchQueryJSON?.groupBy).toBe(CONST.SEARCH.GROUP_BY.MONTH); + + // Check that date filter with year-to-date preset exists in flatFilters + const dateFilter = searchQueryJSON?.flatFilters?.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); + expect(dateFilter).toBeDefined(); + expect(dateFilter?.filters?.some((f) => f.value === CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE)).toBe(true); + + expect(searchQueryJSON?.view).toBe(CONST.SEARCH.VIEW.BAR); + expect(searchQueryJSON?.sortBy).toBe(CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH); + expect(searchQueryJSON?.sortOrder).toBe(CONST.SEARCH.SORT_ORDER.DESC); + }); + + test('Should return Spend Over Time search with valid hash', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined); + const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]; + + expect(spendOverTimeSearch.hash).toBeGreaterThan(0); + expect(spendOverTimeSearch.similarSearchHash).toBeGreaterThan(0); + }); + + test('Should return Spend Over Time search query string with correct format', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined); + const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]; + const searchQuery = spendOverTimeSearch.searchQuery; + + expect(searchQuery).toContain(`type:${CONST.SEARCH.DATA_TYPES.EXPENSE}`); + expect(searchQuery).toContain(`groupBy:${CONST.SEARCH.GROUP_BY.MONTH}`); + expect(searchQuery).toContain(`date:${CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE}`); + expect(searchQuery).toContain(`view:${CONST.SEARCH.VIEW.BAR}`); + expect(searchQuery).toContain(`sortBy:${CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH}`); + expect(searchQuery).toContain(`sortOrder:${CONST.SEARCH.SORT_ORDER.DESC}`); + }); + }); +}); From 8adc4a740da75319e8efa1aff1f0a93fe97adff8 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Fri, 30 Jan 2026 15:42:06 -0800 Subject: [PATCH 5/5] Fix tests --- tests/unit/Search/SearchUIUtilsTest.ts | 169 ++++++++++++++++++++++- tests/unit/Search/SpendOverTimeTest.ts | 180 ------------------------- 2 files changed, 166 insertions(+), 183 deletions(-) delete mode 100644 tests/unit/Search/SpendOverTimeTest.ts diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 0dc7c41788c2a..76196b9305c94 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -5145,6 +5145,168 @@ describe('SearchUIUtils', () => { const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined); expect(response.topCategories).toBe(false); }); + + test('Should show Spend Over Time for Admin role in paid policy', () => { + const policyKey = `policy_${policyID}`; + + const policies: OnyxCollection = { + [policyKey]: { + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.ADMIN, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined); + expect(response.spendOverTime).toBe(true); + }); + + test('Should show Spend Over Time for Auditor role in paid policy', () => { + const policyKey = `policy_${policyID}`; + + const policies: OnyxCollection = { + [policyKey]: { + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.AUDITOR, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility('auditor@policy.com', {}, policies, undefined); + expect(response.spendOverTime).toBe(true); + }); + + test('Should show Spend Over Time for Approver role in paid policy', () => { + const policyKey = `policy_${policyID}`; + + const policies: OnyxCollection = { + [policyKey]: { + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + approver: approverEmail, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(approverEmail, {}, policies, undefined); + expect(response.spendOverTime).toBe(true); + }); + + test('Should hide Spend Over Time for User role in paid policy', () => { + const policyKey = `policy_${policyID}`; + + const policies: OnyxCollection = { + [policyKey]: { + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.USER, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility('user@policy.com', {}, policies, undefined); + expect(response.spendOverTime).toBe(false); + }); + + test('Should hide Spend Over Time for free policies even with Admin role', () => { + const policyKey = `policy_${policyID}`; + + const policies: OnyxCollection = { + [policyKey]: { + id: policyID, + type: CONST.POLICY.TYPE.PERSONAL, + role: CONST.POLICY.ROLE.ADMIN, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined); + expect(response.spendOverTime).toBe(false); + }); + + test('Should show Spend Over Time if at least one policy has Admin/Auditor/Approver role', () => { + const policies: OnyxCollection = { + policyOne: { + id: 'policyOne', + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.USER, + } as OnyxTypes.Policy, + policyTwo: { + id: 'policyTwo', + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.ADMIN, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined); + expect(response.spendOverTime).toBe(true); + }); + + test('Should hide Spend Over Time if all policies have User role', () => { + const policies: OnyxCollection = { + policyOne: { + id: 'policyOne', + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.USER, + } as OnyxTypes.Policy, + policyTwo: { + id: 'policyTwo', + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.USER, + } as OnyxTypes.Policy, + }; + + const response = SearchUIUtils.getSuggestedSearchesVisibility('user@policy.com', {}, policies, undefined); + expect(response.spendOverTime).toBe(false); + }); + + test('Should return Spend Over Time search with correct properties', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined); + const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]; + + expect(spendOverTimeSearch).toBeDefined(); + expect(spendOverTimeSearch.key).toBe(CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME); + expect(spendOverTimeSearch.translationPath).toBe('search.spendOverTime'); + expect(spendOverTimeSearch.type).toBe(CONST.SEARCH.DATA_TYPES.EXPENSE); + expect(spendOverTimeSearch.icon).toBe('Receipt'); + }); + + test('Should return Spend Over Time search query with correct parameters', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined); + const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]; + const searchQueryJSON = spendOverTimeSearch.searchQueryJSON; + + expect(searchQueryJSON).toBeDefined(); + expect(searchQueryJSON?.type).toBe(CONST.SEARCH.DATA_TYPES.EXPENSE); + expect(searchQueryJSON?.groupBy).toBe(CONST.SEARCH.GROUP_BY.MONTH); + + // Check that date filter with year-to-date preset exists in flatFilters + const dateFilter = searchQueryJSON?.flatFilters?.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); + expect(dateFilter).toBeDefined(); + expect(dateFilter?.filters?.some((f) => f.value === CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE)).toBe(true); + + expect(searchQueryJSON?.view).toBe(CONST.SEARCH.VIEW.BAR); + expect(searchQueryJSON?.sortBy).toBe(CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH); + expect(searchQueryJSON?.sortOrder).toBe(CONST.SEARCH.SORT_ORDER.DESC); + }); + + test('Should return Spend Over Time search with valid hash', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined); + const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]; + + expect(spendOverTimeSearch.hash).toBeGreaterThan(0); + expect(spendOverTimeSearch.similarSearchHash).toBeGreaterThan(0); + }); + + test('Should return Spend Over Time search query string with correct format', () => { + const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined); + const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]; + const searchQuery = spendOverTimeSearch.searchQuery; + + expect(searchQuery).toContain(`type:${CONST.SEARCH.DATA_TYPES.EXPENSE}`); + expect(searchQuery).toContain(`groupBy:${CONST.SEARCH.GROUP_BY.MONTH}`); + expect(searchQuery).toContain(`date:${CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE}`); + expect(searchQuery).toContain(`view:${CONST.SEARCH.VIEW.BAR}`); + expect(searchQuery).toContain(`sortBy:${CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH}`); + expect(searchQuery).toContain(`sortOrder:${CONST.SEARCH.SORT_ORDER.DESC}`); + }); }); describe('Test getColumnsToShow', () => { @@ -6242,11 +6404,12 @@ describe('SearchUIUtils', () => { }); describe('view autocomplete values', () => { - test('should include all view values (table, bar)', () => { + test('should include all view values (table, bar, line)', () => { const viewValues = Object.values(CONST.SEARCH.VIEW); expect(viewValues).toContain('table'); expect(viewValues).toContain('bar'); - expect(viewValues).toHaveLength(2); + expect(viewValues).toContain('line'); + expect(viewValues).toHaveLength(3); }); test('should correctly map view values to user-friendly values', () => { @@ -6254,7 +6417,7 @@ describe('SearchUIUtils', () => { const userFriendlyValues = viewValues.map((value) => getUserFriendlyValue(value)); // All view values should be mapped (they may be the same or different) - expect(userFriendlyValues).toHaveLength(2); + expect(userFriendlyValues).toHaveLength(3); expect(userFriendlyValues.every((value) => typeof value === 'string')).toBe(true); }); }); diff --git a/tests/unit/Search/SpendOverTimeTest.ts b/tests/unit/Search/SpendOverTimeTest.ts deleted file mode 100644 index 254c90448d372..0000000000000 --- a/tests/unit/Search/SpendOverTimeTest.ts +++ /dev/null @@ -1,180 +0,0 @@ -import type {OnyxCollection} from 'react-native-onyx'; -import CONST from '@src/CONST'; -import * as SearchUIUtils from '@src/libs/SearchUIUtils'; -import type * as OnyxTypes from '@src/types/onyx'; - -const adminAccountID = 18439984; -const adminEmail = 'admin@policy.com'; -const auditorEmail = 'auditor@policy.com'; -const approverEmail = 'approver@policy.com'; -const userEmail = 'user@policy.com'; -const policyID = 'A1B2C3'; - -describe('Test Spend Over Time Search', () => { - - describe('Test getSuggestedSearchesVisibility for Spend Over Time', () => { - test('Should show Spend Over Time for Admin role in paid policy', () => { - const policyKey = `policy_${policyID}`; - - const policies: OnyxCollection = { - [policyKey]: { - id: policyID, - type: CONST.POLICY.TYPE.TEAM, - role: CONST.POLICY.ROLE.ADMIN, - } as OnyxTypes.Policy, - }; - - const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined); - expect(response.spendOverTime).toBe(true); - }); - - test('Should show Spend Over Time for Auditor role in paid policy', () => { - const policyKey = `policy_${policyID}`; - - const policies: OnyxCollection = { - [policyKey]: { - id: policyID, - type: CONST.POLICY.TYPE.TEAM, - role: CONST.POLICY.ROLE.AUDITOR, - } as OnyxTypes.Policy, - }; - - const response = SearchUIUtils.getSuggestedSearchesVisibility(auditorEmail, {}, policies, undefined); - expect(response.spendOverTime).toBe(true); - }); - - test('Should show Spend Over Time for Approver role in paid policy', () => { - const policyKey = `policy_${policyID}`; - - const policies: OnyxCollection = { - [policyKey]: { - id: policyID, - type: CONST.POLICY.TYPE.TEAM, - approver: approverEmail, - } as OnyxTypes.Policy, - }; - - const response = SearchUIUtils.getSuggestedSearchesVisibility(approverEmail, {}, policies, undefined); - expect(response.spendOverTime).toBe(true); - }); - - test('Should hide Spend Over Time for User role in paid policy', () => { - const policyKey = `policy_${policyID}`; - - const policies: OnyxCollection = { - [policyKey]: { - id: policyID, - type: CONST.POLICY.TYPE.TEAM, - role: CONST.POLICY.ROLE.USER, - } as OnyxTypes.Policy, - }; - - const response = SearchUIUtils.getSuggestedSearchesVisibility(userEmail, {}, policies, undefined); - expect(response.spendOverTime).toBe(false); - }); - - test('Should hide Spend Over Time for free policies even with Admin role', () => { - const policyKey = `policy_${policyID}`; - - const policies: OnyxCollection = { - [policyKey]: { - id: policyID, - type: CONST.POLICY.TYPE.PERSONAL, - role: CONST.POLICY.ROLE.ADMIN, - } as OnyxTypes.Policy, - }; - - const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined); - expect(response.spendOverTime).toBe(false); - }); - - test('Should show Spend Over Time if at least one policy has Admin/Auditor/Approver role', () => { - const policies: OnyxCollection = { - policyOne: { - id: 'policyOne', - type: CONST.POLICY.TYPE.TEAM, - role: CONST.POLICY.ROLE.USER, - } as OnyxTypes.Policy, - policyTwo: { - id: 'policyTwo', - type: CONST.POLICY.TYPE.TEAM, - role: CONST.POLICY.ROLE.ADMIN, - } as OnyxTypes.Policy, - }; - - const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined); - expect(response.spendOverTime).toBe(true); - }); - - test('Should hide Spend Over Time if all policies have User role', () => { - const policies: OnyxCollection = { - policyOne: { - id: 'policyOne', - type: CONST.POLICY.TYPE.TEAM, - role: CONST.POLICY.ROLE.USER, - } as OnyxTypes.Policy, - policyTwo: { - id: 'policyTwo', - type: CONST.POLICY.TYPE.TEAM, - role: CONST.POLICY.ROLE.USER, - } as OnyxTypes.Policy, - }; - - const response = SearchUIUtils.getSuggestedSearchesVisibility(userEmail, {}, policies, undefined); - expect(response.spendOverTime).toBe(false); - }); - }); - - describe('Test getSuggestedSearches for Spend Over Time', () => { - test('Should return Spend Over Time search with correct properties', () => { - const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined); - const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]; - - expect(spendOverTimeSearch).toBeDefined(); - expect(spendOverTimeSearch.key).toBe(CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME); - expect(spendOverTimeSearch.translationPath).toBe('search.spendOverTime'); - expect(spendOverTimeSearch.type).toBe(CONST.SEARCH.DATA_TYPES.EXPENSE); - expect(spendOverTimeSearch.icon).toBe('Receipt'); - }); - - test('Should return Spend Over Time search query with correct parameters', () => { - const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined); - const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]; - const searchQueryJSON = spendOverTimeSearch.searchQueryJSON; - - expect(searchQueryJSON).toBeDefined(); - expect(searchQueryJSON?.type).toBe(CONST.SEARCH.DATA_TYPES.EXPENSE); - expect(searchQueryJSON?.groupBy).toBe(CONST.SEARCH.GROUP_BY.MONTH); - - // Check that date filter with year-to-date preset exists in flatFilters - const dateFilter = searchQueryJSON?.flatFilters?.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); - expect(dateFilter).toBeDefined(); - expect(dateFilter?.filters?.some((f) => f.value === CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE)).toBe(true); - - expect(searchQueryJSON?.view).toBe(CONST.SEARCH.VIEW.BAR); - expect(searchQueryJSON?.sortBy).toBe(CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH); - expect(searchQueryJSON?.sortOrder).toBe(CONST.SEARCH.SORT_ORDER.DESC); - }); - - test('Should return Spend Over Time search with valid hash', () => { - const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined); - const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]; - - expect(spendOverTimeSearch.hash).toBeGreaterThan(0); - expect(spendOverTimeSearch.similarSearchHash).toBeGreaterThan(0); - }); - - test('Should return Spend Over Time search query string with correct format', () => { - const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined); - const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]; - const searchQuery = spendOverTimeSearch.searchQuery; - - expect(searchQuery).toContain(`type:${CONST.SEARCH.DATA_TYPES.EXPENSE}`); - expect(searchQuery).toContain(`groupBy:${CONST.SEARCH.GROUP_BY.MONTH}`); - expect(searchQuery).toContain(`date:${CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE}`); - expect(searchQuery).toContain(`view:${CONST.SEARCH.VIEW.BAR}`); - expect(searchQuery).toContain(`sortBy:${CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH}`); - expect(searchQuery).toContain(`sortOrder:${CONST.SEARCH.SORT_ORDER.DESC}`); - }); - }); -});