diff --git a/cspell.json b/cspell.json index 77fa42526dfe..3795a453b236 100644 --- a/cspell.json +++ b/cspell.json @@ -1242,6 +1242,7 @@ "ignorePaths": [ ".github/actions/javascript/**/index.js", ".gitignore", + "tests/unit/__snapshots__/OptionsListUtilsSearchParityTest.ts.snap", "src/languages/de.ts", "src/languages/el.ts", "src/languages/es.ts", diff --git a/tests/unit/OptionsListUtilsSearchParityTest.ts b/tests/unit/OptionsListUtilsSearchParityTest.ts new file mode 100644 index 000000000000..5b70e2a224cc --- /dev/null +++ b/tests/unit/OptionsListUtilsSearchParityTest.ts @@ -0,0 +1,457 @@ +// cspell:disable -- names and queries deliberately use diacritics, non-Latin scripts and misspelled forms +import SEARCH_ROUTER_OPTIONS_CONFIG from '@components/Search/SearchRouter/searchRouterOptionsConfig'; + +import type {OptionList, SearchOptionData} from '@libs/OptionsListUtils'; +import { + clearAlternateTextCache, + clearFilteredOptionListCache, + combineOrderingOfReportsAndPersonalDetails, + createFilteredOptionList, + getSearchOptions, + hydrateContactOption, +} from '@libs/OptionsListUtils'; +import type {OptionsResult} from '@libs/OptionsListUtils/types'; + +import initOnyxDerivedValues from '@userActions/OnyxDerived'; + +import CONST from '@src/CONST'; +import IntlStore from '@src/languages/IntlStore'; +import Navigation from '@src/libs/Navigation/Navigation'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report, Rule} from '@src/types/onyx'; + +import type {OnyxCollection} from 'react-native-onyx'; +import type {TupleToUnion} from 'type-fest'; + +import Onyx from 'react-native-onyx'; + +import type {SearchFixture} from '../utils/searchParity/generateSearchFixture'; + +import {ACCOUNT_IDS, CURRENT_USER_EMAIL, generateSearchFixture, POLICY_IDS, SEARCH_QUERIES, writeSearchFixtureToOnyx} from '../utils/searchParity/generateSearchFixture'; +import {convertToDisplayString, convertToDisplayStringWithoutCurrency, formatPhoneNumber, localeCompare, translateLocal} from '../utils/TestHelper'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +jest.mock('@src/libs/Navigation/Navigation', () => ({ + navigate: jest.fn(), + getActiveRouteWithoutParams: jest.fn(() => ''), + dismissModal: jest.fn(), + goBack: jest.fn(), + getTopmostReportId: jest.fn(() => undefined), + setNavigationActionToMicrotaskQueue: jest.fn(), + isNavigationReady: jest.fn(() => Promise.resolve()), + getReportRHPActiveRoute: jest.fn(), +})); + +// Baseline snapshot for the search option pipeline (createFilteredOptionList to getSearchOptions). +// Only re-record (jest -u) against pre-migration search on main so the migration never becomes its own baseline. +const fixture = generateSearchFixture(); + +/** Drops every cache the search keeps between calls, so the next build starts cold. */ +function clearSearchCaches() { + clearFilteredOptionListCache(); + clearAlternateTextCache(); +} + +/** + * Serializes options into " [flags] [alt: ...] [sub: ...] [accID: ...] [nav: ...]" lines to pin option ordering and content rather than rendering details. + * [sub: ...] is only emitted for rooms and workspace chats, because that is what search matches for them. + * [nav: ...] is the row's navigation target (reportID, else login, else none), emitted only when it differs from keyForList: report rows navigate by reportID, which equals keyForList today, so only a divergence shows. + * The userToInvite login target is not emitted, because it is already visible as the text or the trailing invited login. + * participantsList, icons and ownerAccountID are left out because the SearchRouter only renders them or never reads them. policyID and item are read for contextual search and FullStory masking, but only the canonical builders set them, so they are left out too. + */ +function serializeOption(option: SearchOptionData | null | undefined, {isUserToInvite = false}: {isUserToInvite?: boolean} = {}): string | null { + if (!option) { + return null; + } + const flags = [ + option.isSelected ? ' [selected]' : '', + option.isDisabled ? ' [disabled]' : '', + option.isBold ? ' [bold]' : '', + option.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.INFO ? ' [gbr]' : '', + option.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR ? ' [rbr]' : '', + option.private_isArchived ? ' [archived]' : '', + ].join(''); + const invitedLogin = isUserToInvite && option.login !== option.text ? ` ${option.login ?? ''}` : ''; + const altText = option.alternateText ? ` [alt: ${option.alternateText}]` : ''; + const subtitle = (option.isChatRoom || option.isPolicyExpenseChat) && option.subtitle ? ` [sub: ${option.subtitle}]` : ''; + const accountID = option.accountID ? ` [accID: ${option.accountID}]` : ''; + const navigationTarget = option.reportID || option.login; + const isLoginShownAlready = isUserToInvite && !option.reportID; + const navigation = !isLoginShownAlready && String(navigationTarget ?? '') !== (option.keyForList ?? '') ? ` [nav: ${navigationTarget || 'none'}]` : ''; + return `${option.keyForList ?? ''} ${option.text ?? ''}${flags}${altText}${subtitle}${accountID}${navigation}${invitedLogin}`; +} + +/** What the screen shows: one flat list of rows, in order. */ +type DisplayedResult = {displayedRows: SearchOptionData[]; hasMore?: boolean}; + +function serializeResult(result: OptionsResult | DisplayedResult) { + if ('displayedRows' in result) { + return { + displayedRows: result.displayedRows.map((option) => serializeOption(option)), + hasMore: !!result.hasMore, + }; + } + const {options, hasMore} = result; + return { + recentReports: options.recentReports.map((option) => serializeOption(option)), + personalDetails: options.personalDetails.map((option) => serializeOption(option)), + workspaceChats: options.workspaceChats?.map((option) => serializeOption(option)), + selfDMChat: serializeOption(options.selfDMChat), + currentUserOption: serializeOption(options.currentUserOption), + userToInvite: serializeOption(options.userToInvite, { + isUserToInvite: true, + }), + hasMore: !!hasMore, + }; +} + +/** Preserves pipeline ordering, but sorts unordered collections to ensure deterministic snapshots. */ +function serializeOptionList(optionList: OptionList, {isOrderMeaningful}: {isOrderMeaningful: boolean}) { + const sortIfUnordered = (references: Array) => (isOrderMeaningful ? references : [...references].sort()); + return { + reports: sortIfUnordered(optionList.reports.map((option) => serializeOption(option))), + + // Shells are compared both as built and after hydration, so the shell/hydrator split stays invisible + personalDetails: optionList.personalDetails.map((shell) => `${shell.reportID} | ${shell.accountID} | ${shell.login ?? ''}`).sort(), + hydratedPersonalDetails: optionList.personalDetails.map((shell) => serializeOption(hydrateContactOption(shell))).sort(), + }; +} + +/** + * Cap for the SearchRouter idle list recorded in optionLists, which only exists to record evictions in createFilteredOptionList. No call site searches this list. + * The production cap (SEARCH_ROUTER_OPTIONS_CONFIG.maxRecentReports) keeps all fixture reports, while setting this to 10 forces evictions. + * The value is arbitrary: any cap below the fixture report count forces evictions, and 10 just keeps the recorded idle list short. No particular sort rule sits at the boundary. + */ +const SEARCH_ROUTER_IDLE_REPORTS_CAP = 10; + +// The fixture account has no intro selection (not a track-intent user), no Onyx rules and no policy tags, which is what SearchAutocompleteList reads for it. +// allPolicyTags is omitted because it only reaches modified-expense previews and the fixture has none, so changes to tag names, rules or the track-intent welcome text are not pinned here. +const RULES: OnyxCollection = {}; + +type BuildOptionListConfig = { + isSearching: boolean; + maxRecentReports?: number; + deferContactsUntilSearch?: boolean; + includeP2P?: boolean; +}; + +/** Builds the list as useFilteredOptions does, keeping whatever search cached from earlier builds. */ +function buildOptionList(sourceFixture: SearchFixture, {isSearching, maxRecentReports, deferContactsUntilSearch, includeP2P = true}: BuildOptionListConfig): OptionList { + const {sortedActions, lastActions, transactionThreadIDs} = sourceFixture.derived.sortedReportActions; + return createFilteredOptionList( + sourceFixture.personalDetails, + sourceFixture.reports, + sourceFixture.derived.reportAttributes, + sourceFixture.privateIsArchivedMap, + sourceFixture.policies, + { + currentUserAccountID: ACCOUNT_IDS.CURRENT_USER, + currentUserLogin: CURRENT_USER_EMAIL, + transactionThreadIDs, + lastActions, + dateFnsLocale: undefined, + convertToDisplayString, + conciergeReportID: sourceFixture.conciergeReportID, + includeP2P, + isSearching, + maxRecentReports, + deferContactsUntilSearch, + formatPhoneNumber, + }, + RULES, + undefined, + undefined, + false, + sortedActions, + ); +} + +function buildSearchRouterIdleOptionList(sourceFixture: SearchFixture, maxRecentReports: number): OptionList { + return buildOptionList(sourceFixture, { + isSearching: false, + maxRecentReports, + deferContactsUntilSearch: SEARCH_ROUTER_OPTIONS_CONFIG.deferContactsUntilSearch, + includeP2P: SEARCH_ROUTER_OPTIONS_CONFIG.includeP2P, + }); +} + +// Passed to combineOrderingOfReportsAndPersonalDetails. Only affects workspace chat ordering, which displayed passes do not show. +const ACTIVE_POLICY_ID = POLICY_IDS.OWN; + +type SearchCallSite = (options: OptionList, query: string) => OptionsResult | DisplayedResult; + +const CALL_SITE_NAMES = ['searchRouterLocal', 'searchRouterDisplayed'] as const; + +type SearchCallSiteName = TupleToUnion; + +/** + * Keeps getReportByID stable for identical fixture.reports references as useCallback(..., [allReports]) does in useFilteredOptions, + * so the preview cache is not invalidated between keystrokes when reports have not changed. + */ +const reportGettersByReports = new WeakMap Report | undefined>(); + +function getOrCreateReportByID(reports: SearchFixture['reports']) { + let getter = reportGettersByReports.get(reports); + if (!getter) { + getter = (reportID: string | undefined): Report | undefined => (reportID ? (reports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? undefined) : undefined); + reportGettersByReports.set(reports, getter); + } + return getter; +} + +/** + * Builds frozen copies of the production SearchRouter search configurations for the given fixture. + * Includes the screen-side ranking step (combineOrderingOfReportsAndPersonalDetails) while omitting pure styling steps. + */ +function createCallSites(sourceFixture: SearchFixture) { + const getReportByID = getOrCreateReportByID(sourceFixture.reports); + const {sortedActions, lastActions, transactionThreadIDs} = sourceFixture.derived.sortedReportActions; + const visibleReportActionsData = sourceFixture.derived.visibleReportActions; + + // Configuration every getSearchOptions screen passes + const baseSearchConfig = { + dateFnsLocale: undefined, + convertToDisplayString, + draftComments: sourceFixture.draftComments, + countryCode: CONST.DEFAULT_COUNTRY_CODE, + loginList: sourceFixture.loginList, + currentUserAccountID: ACCOUNT_IDS.CURRENT_USER, + currentUserEmail: CURRENT_USER_EMAIL, + policyCollection: sourceFixture.policies, + personalDetails: sourceFixture.personalDetails, + sortedActions, + conciergeReportID: sourceFixture.conciergeReportID, + translate: translateLocal, + formatPhoneNumber, + getReportByID, + rules: RULES, + isTrackIntentUser: false, + }; + + // SearchAutocompleteList server pass + const serverPassConfig = { + ...baseSearchConfig, + visibleReportActionsData, + }; + + // SearchAutocompleteList local pass + const commonSearchConfig = { + ...baseSearchConfig, + convertToDisplayStringWithoutCurrency, + currentUserLogin: CURRENT_USER_EMAIL, + visibleReportActionsData, + transactionThreadIDs, + lastActions, + reportAttributesDerived: sourceFixture.derived.reportAttributes, + localeCompare, + }; + + // SearchAutocompleteList's local pass, which its displayed pass then reorders + const runSearchRouterSearch = (options: OptionList, query: string) => + getSearchOptions({ + ...commonSearchConfig, + options, + isDefaultRoomsBetaEnabled: true, + isUsedInChatFinder: true, + includeReadOnly: true, + searchQuery: query, + maxResults: CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS, + includeUserToInvite: true, + includeRecentReports: true, + includeCurrentUser: true, + shouldShowGBR: false, + shouldUnreadBeBold: true, + }); + + const searchCallSites = { + searchRouterLocal: (options, query) => runSearchRouterSearch(options, query), + + // Replicates the displayed ranking and capping pass from SearchAutocompleteList + searchRouterDisplayed: (options, query) => { + const result = runSearchRouterSearch(options, query); + + if (!query.trim()) { + return { + displayedRows: result.options.recentReports.slice(0, CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS), + hasMore: result.hasMore, + }; + } + + // SearchAutocompleteList ranks by the raw autocomplete value, not the trimmed one + const ordered = combineOrderingOfReportsAndPersonalDetails(result.options, query, ACTIVE_POLICY_ID, { + sortByReportTypeInSearch: true, + preferChatRoomsOverThreads: true, + }); + const displayedReports = [...ordered.recentReports, ...ordered.personalDetails]; + if (result.options.userToInvite) { + displayedReports.push(result.options.userToInvite); + } + + return { + displayedRows: displayedReports.slice(0, CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS), + hasMore: result.hasMore, + }; + }, + } satisfies Record; + + return {searchCallSites, serverPassConfig}; +} + +// Reports returned by the SearchAutocompleteList server pass in Auth ranking order, including a self-DM that the pass drops and reports the canonical filter rejects. +const SERVER_PASS_REPORT_NAMES = [ + '#admins', + '#announce', + 'Hidden thread', + '#vít-room', + 'Weekend Hiking', + 'Current User (you)', + 'Empty thread', + 'Book flights', + 'Offsite expenses', + 'Empty submitted report', + 'Thread about the offsite', + 'İsmail Yılmaz', + "Mary-Jane O'Brien (archived) ", +]; + +// An ID Auth returns for a report the client has not loaded, which the pass drops because it has no local option. No fixture report uses it. +const UNKNOWN_SERVER_REPORT_ID = '999999'; + +/** Finds a fixture report by its name, so a test never depends on the reportIDs the fixture assigns. */ +function getReportIDByName(sourceFixture: SearchFixture, reportName: string): string { + const reportID = Object.values(sourceFixture.reports).find((report) => report?.reportName === reportName)?.reportID; + if (!reportID) { + throw new Error(`No fixture report is named ${reportName}`); + } + return reportID; +} + +// Dropped from the list unless focused, because the focused report skips the checks that drop empty threads and empty submitted reports +const FOCUSED_REPORT_NAMES = ['Empty thread', 'Empty submitted report']; + +// The thread ranks past the MAX_AMOUNT_OF_SUGGESTIONS rows the blank list shows, so its focused blank result equals the unfocused one and only the query is recorded for it +const FOCUSED_REPORT_NAME_WITH_BLANK = 'Empty submitted report'; + +// Matches both focused reports, so the pass shows whether they come back while a query is typed +const FOCUSED_REPORT_QUERY = 'empty'; + +/** Runs with the report focused, as when Cmd+K opens over it, then goes back to nothing focused so no other pass sees it. */ +function withFocusedReport(reportID: string, run: () => T): T { + jest.mocked(Navigation.getTopmostReportId).mockReturnValue(reportID); + try { + return run(); + } finally { + jest.mocked(Navigation.getTopmostReportId).mockReturnValue(undefined); + } +} + +/** Computes every recorded pass over the fixture, starting from cold caches. */ +function computeGoldenMaster(sourceFixture: SearchFixture) { + const {searchCallSites, serverPassConfig} = createCallSites(sourceFixture); + clearSearchCaches(); + + const searching = buildOptionList(sourceFixture, {isSearching: true}); + const buildSearchRouterIdleList = (maxRecentReports: number) => buildSearchRouterIdleOptionList(sourceFixture, maxRecentReports); + const searchRouterIdle = buildSearchRouterIdleList(SEARCH_ROUTER_IDLE_REPORTS_CAP); + const searchRouterIdleAtProductionCap = buildSearchRouterIdleList(SEARCH_ROUTER_OPTIONS_CONFIG.maxRecentReports); + + const optionLists = { + // The searching list keeps the collection's iteration order, which the search ranks later, so only its content is recorded + searching: serializeOptionList(searching, {isOrderMeaningful: false}), + searchRouterIdle: serializeOptionList(searchRouterIdle, { + isOrderMeaningful: true, + }), + searchRouterIdleAtProductionCap: serializeOptionList(searchRouterIdleAtProductionCap, {isOrderMeaningful: true}), + }; + + // The SearchRouter starts at the production cap, and its loadAll effect expands a list that has fewer than 20 rows, so a blank query never searches the small idle list + const getCallSiteOptionList = (query: string) => (query.trim() ? searching : searchRouterIdleAtProductionCap); + const callSites = Object.fromEntries( + CALL_SITE_NAMES.map((callSite) => [ + callSite, + Object.fromEntries(SEARCH_QUERIES.map((query) => [query, serializeResult(searchCallSites[callSite](getCallSiteOptionList(query), query))])), + ]), + ); + + const orderedServerReportIDs = [...SERVER_PASS_REPORT_NAMES.map((reportName) => getReportIDByName(sourceFixture, reportName)), UNKNOWN_SERVER_REPORT_ID].slice( + 0, + CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS, + ); + const serverReportIDs = new Set(orderedServerReportIDs); + + // SearchAutocompleteList server pass reads derived attributes from Onyx because reportAttributesDerived is omitted + const serverPassOptions = getSearchOptions({ + ...serverPassConfig, + options: { + reports: searching.reports.filter((option) => serverReportIDs.has(option.reportID)), + personalDetails: [], + }, + isDefaultRoomsBetaEnabled: true, + isUsedInChatFinder: true, + includeReadOnly: true, + searchQuery: '', + maxResults: orderedServerReportIDs.length, + includeUserToInvite: false, + includeRecentReports: true, + includeCurrentUser: false, + shouldShowGBR: false, + shouldUnreadBeBold: true, + }).options; + + // The pass then puts the rows back in Auth's order and drops the self-DM + const serverPassOptionsByReportID = new Map(serverPassOptions.recentReports.map((option) => [option.reportID, option])); + const serverPass = orderedServerReportIDs.map((reportID) => serverPassOptionsByReportID.get(reportID)).filter((option) => !!option && !option.isSelfDM); + + const passes = { + serverPass: serverPass.map((option) => serializeOption(option)), + + // Recorded after the passes above, which run with nothing focused + focusedReport: Object.fromEntries( + FOCUSED_REPORT_NAMES.map((reportName) => [ + reportName, + withFocusedReport(getReportIDByName(sourceFixture, reportName), () => ({ + ...(reportName === FOCUSED_REPORT_NAME_WITH_BLANK ? {blank: serializeResult(searchCallSites.searchRouterDisplayed(searchRouterIdleAtProductionCap, ''))} : {}), + [FOCUSED_REPORT_QUERY]: serializeResult(searchCallSites.searchRouterDisplayed(searching, FOCUSED_REPORT_QUERY)), + })), + ]), + ), + }; + + return {optionLists, callSites, passes}; +} + +describe('OptionsListUtils search parity (golden master)', () => { + let goldenMaster: ReturnType; + + beforeAll(async () => { + Onyx.init({keys: ONYXKEYS}); + await IntlStore.load(CONST.LOCALES.EN); + initOnyxDerivedValues(); + await waitForBatchedUpdates(); + + // The fixture starts without derived values. Writing to Onyx derives them for subsequent reads. + fixture.derived = await writeSearchFixtureToOnyx(fixture); + goldenMaster = computeGoldenMaster(fixture); + }); + + it('builds searching and SearchRouter idle option lists that match the golden master', () => { + // Given the fixture account with every report type and contact quirk + // When the option list is built for search (every report) and for the SearchRouter idle state (top-N, contacts deferred) at the small cap and the production cap + // Then every option keeps its identity, flags and, where it matters, its order + expect(goldenMaster.optionLists).toMatchSnapshot(); + }); + + it.each(CALL_SITE_NAMES)('call site %s matches the golden master for every query', (callSite) => { + // Given a production getSearchOptions config and the option list it searches (the full list, or the production-cap idle list while the query is blank) + // When every query in the matrix runs through it + // Then the sections and their order match the recorded golden master + expect(goldenMaster.callSites[callSite]).toMatchSnapshot(); + }); + + it('SearchRouter blank query and server-ID pass match the golden master', () => { + // Given the SearchAutocompleteList server pass (a fixed set of reports in Auth's order, no query, including rejected and unknown IDs that it drops) and the SearchRouter opened over a focused empty thread or empty submitted report + // When they run through their production configs, the focused ones for a query matching the focused report, and for a blank query only where the report lands within the displayed rows + // Then all match the recorded golden master, with the focused report shown only while it is focused + expect(goldenMaster.passes).toMatchSnapshot(); + }); +}); diff --git a/tests/unit/__snapshots__/OptionsListUtilsSearchParityTest.ts.snap b/tests/unit/__snapshots__/OptionsListUtilsSearchParityTest.ts.snap new file mode 100644 index 000000000000..a5f14bfa6fda --- /dev/null +++ b/tests/unit/__snapshots__/OptionsListUtilsSearchParityTest.ts.snap @@ -0,0 +1,1774 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OptionsListUtils search parity (golden master) SearchRouter blank query and server-ID pass match the golden master 1`] = ` +{ + "focusedReport": { + "Empty submitted report": { + "blank": { + "displayedRows": [ + "2004 Current User (you) [alt: This is your personal space. Use it for notes, tasks, drafts, and reminders.] [accID: 1]", + "2013 #orphan-room [alt: Gone Workspace • This chat room is for anything #orphan-room related.] [sub: Gone Workspace]", + "1124 Ada Cole [alt: This chat is with Ada Cole.] [accID: 124]", + "1103 Dan Ames [alt: This chat is with Dan Ames.] [accID: 103]", + "1019 No Login [alt: This chat is with No Login.] [accID: 19]", + "1123 Lou Byrd [alt: This chat is with Lou Byrd.] [accID: 123]", + "1102 Cyd Ames [alt: This chat is with Cyd Ames.] [accID: 102]", + "1018 Domain Admin [alt: This chat is with Domain Admin.] [accID: 18]", + "1121 Jon Byrd [alt: This chat is with Jon Byrd.] [accID: 121]", + "1100 Ada Ames [alt: This chat is with Ada Ames.] [accID: 100]", + "1120 Ivy Byrd [alt: This chat is with Ivy Byrd.] [accID: 120]", + "1015 Alex Smith Jr. [alt: This chat is with Alex Smith Jr..] [accID: 15]", + "2012 #vít-room [rbr] [alt: Acme Holdings • This chat room is for anything #vít-room related.] [sub: Acme Holdings]", + "2032 Empty submitted report [alt: No activity yet] [accID: 1]", + "1139 Dan Dunn [alt: This chat is with Dan Dunn.] [accID: 139]", + "1118 Gus Byrd [alt: This chat is with Gus Byrd.] [accID: 118]", + "1013 (500) 555-0006 [alt: This chat is with (500) 555-0006.] [accID: 13]", + "2007 Groß Straße [alt: Vít Workspace • This chat is for invoices between Groß Straße and Vít Workspace. Use the + button to send an invoice.] [sub: Invoices]", + "1138 Cyd Dunn [alt: This chat is with Cyd Dunn.] [accID: 138]", + "1117 Fay Byrd [alt: This chat is with Fay Byrd.] [accID: 117]", + ], + "hasMore": true, + }, + "empty": { + "displayedRows": [ + "2032 Empty submitted report [alt: No activity yet] [accID: 1]", + "2025 Empty pinned thread [alt: No activity yet]", + "2024 Empty thread with a draft [alt: No activity yet]", + ], + "hasMore": false, + }, + }, + "Empty thread": { + "empty": { + "displayedRows": [ + "2025 Empty pinned thread [alt: No activity yet]", + "2024 Empty thread with a draft [alt: No activity yet]", + "2023 Empty thread [alt: No activity yet]", + ], + "hasMore": false, + }, + }, + }, + "serverPass": [ + "2010 #admins [alt: Vít Workspace • This chat is with Vít Workspace admin. Use it to chat about workspace setup and more.] [sub: Vít Workspace]", + "2011 #announce [alt: Vít Workspace • This chat is with everyone in Vít Workspace. Use it for the most important announcements.] [sub: Vít Workspace]", + "2012 #vít-room [rbr] [alt: Acme Holdings • This chat room is for anything #vít-room related.] [sub: Acme Holdings]", + "2003 Weekend Hiking [alt: This chat is with José Ñúñez, Mary-Jane O'Brien, and Vít Novák.]", + "2030 Book flights [alt: No activity yet]", + "2031 Offsite expenses [alt: No activity yet] [accID: 1]", + "2021 Thread about the offsite [bold] [alt: José Ñúñez: Count me in]", + "1004 İsmail Yılmaz [alt: Thanks!] [accID: 4]", + "2000 Mary-Jane O'Brien (archived) [archived] [alt: This chat room has been archived.] [accID: 11]", + ], +} +`; + +exports[`OptionsListUtils search parity (golden master) builds searching and SearchRouter idle option lists that match the golden master 1`] = ` +{ + "searchRouterIdle": { + "hydratedPersonalDetails": [], + "personalDetails": [], + "reports": [ + "2004 Current User (you) [alt: current.user@example.com] [accID: 1]", + "2034 José Ñúñez [sub: Vít Workspace]", + "2001 Alex Smith [alt: alex.smith@example.com] [accID: 14]", + "2013 #orphan-room [sub: Gone Workspace]", + "1124 Ada Cole [alt: ada.cole.24@example.com] [accID: 124]", + "1103 Dan Ames [alt: dan.ames.3@example.com] [accID: 103]", + "1019 No Login [accID: 19]", + "1123 Lou Byrd [alt: lou.byrd.23@example.com] [accID: 123]", + "1102 Cyd Ames [alt: cyd.ames.2@example.com] [accID: 102]", + "1018 Domain Admin [alt: admin@example.com] [accID: 18]", + ], + }, + "searchRouterIdleAtProductionCap": { + "hydratedPersonalDetails": [], + "personalDetails": [], + "reports": [ + "2004 Current User (you) [alt: current.user@example.com] [accID: 1]", + "2034 José Ñúñez [sub: Vít Workspace]", + "2001 Alex Smith [alt: alex.smith@example.com] [accID: 14]", + "2013 #orphan-room [sub: Gone Workspace]", + "1124 Ada Cole [alt: ada.cole.24@example.com] [accID: 124]", + "1103 Dan Ames [alt: dan.ames.3@example.com] [accID: 103]", + "1019 No Login [accID: 19]", + "1123 Lou Byrd [alt: lou.byrd.23@example.com] [accID: 123]", + "1102 Cyd Ames [alt: cyd.ames.2@example.com] [accID: 102]", + "1018 Domain Admin [alt: admin@example.com] [accID: 18]", + "1121 Jon Byrd [alt: jon.byrd.21@example.com] [accID: 121]", + "1100 Ada Ames [alt: ada.ames.0@example.com] [accID: 100]", + "1016 Expensify Notifications [alt: notifications@expensify.com] [accID: 11665625]", + "1120 Ivy Byrd [alt: ivy.byrd.20@example.com] [accID: 120]", + "1015 Alex Smith Jr. [alt: alex.smith2@example.com] [accID: 15]", + "2012 #vít-room [rbr] [sub: Acme Holdings]", + "2032 Empty submitted report [alt: Expense] [accID: 1]", + "1139 Dan Dunn [alt: dan.dunn.39@example.com] [accID: 139]", + "1118 Gus Byrd [alt: gus.byrd.18@example.com] [accID: 118]", + "1013 (500) 555-0006 [alt: (500) 555-0006] [accID: 13]", + "2007 Groß Straße [sub: Invoices]", + "1138 Cyd Dunn [alt: cyd.dunn.38@example.com] [accID: 138]", + "1117 Fay Byrd [alt: fay.byrd.17@example.com] [accID: 117]", + "1012 first.last+tag@x.com [alt: first.last+tag@x.com] [accID: 12]", + "2031 Offsite expenses [alt: Expense] [accID: 1]", + "1136 Ada Dunn [alt: ada.dunn.36@example.com] [accID: 136]", + "1115 Dan Byrd [alt: dan.byrd.15@example.com] [accID: 115]", + "1010 Bob Builder [alt: bob.builder@example.com] [accID: 10]", + "2030 Book flights [alt: Task]", + "1135 Lou Cole [alt: lou.cole.35@example.com] [accID: 135]", + "1114 Cyd Byrd [alt: cyd.byrd.14@example.com] [accID: 114]", + "1009 Ana​Maria [alt: ana.maria@example.com] [accID: 9]", + "2011 #announce [sub: Vít Workspace]", + "2006 Groß Straße's expenses [sub: Vít Workspace]", + "2005 Current User's expenses [gbr] [sub: Vít Workspace • Submits to Current User]", + "1133 Jon Cole [alt: jon.cole.33@example.com] [accID: 133]", + "1112 Ada Byrd [alt: ada.byrd.12@example.com] [accID: 112]", + "1007 Οδυσσεύς [alt: odysseus@example.com] [accID: 7]", + "2002 [accID: 999]", + "1132 Ivy Cole [alt: ivy.cole.32@example.com] [accID: 132]", + "1111 Lou Ames [alt: lou.ames.11@example.com] [accID: 111]", + "1006 Łukasz Wójcik [alt: lukasz@example.com] [accID: 6]", + "2003 Weekend Hiking [alt: Group]", + "2026 Packing list [alt: Thread]", + "2025 Empty pinned thread [alt: Thread]", + "2024 Empty thread with a draft [alt: Thread]", + "2023 Empty thread [alt: Thread]", + "2022 Hidden thread [alt: Thread]", + "2021 Thread about the offsite [alt: Thread]", + "1130 Gus Cole [alt: gus.cole.30@example.com] [accID: 130]", + "1109 Jon Ames [alt: jon.ames.9@example.com] [accID: 109]", + "1004 İsmail Yılmaz [alt: ismail@example.com] [accID: 4]", + "2020 José Ñúñez [alt: jose.nunez@example.com] [accID: 2]", + "2010 #admins [sub: Vít Workspace]", + "1129 Fay Cole [alt: fay.cole.29@example.com] [accID: 129]", + "1108 Ivy Ames [alt: ivy.ames.8@example.com] [accID: 108]", + "1003 Vít Novák [alt: vit.novak@example.com] [accID: 3]", + "1127 Dan Cole [alt: dan.cole.27@example.com] [accID: 127]", + "1106 Gus Ames [alt: gus.ames.6@example.com] [accID: 106]", + "1126 Cyd Cole [alt: cyd.cole.26@example.com] [accID: 126]", + "1105 Fay Ames [alt: fay.ames.5@example.com] [accID: 105]", + "1021 Concierge [alt: concierge@expensify.com] [accID: -1]", + "2000 Mary-Jane O'Brien (archived) [archived] [alt: obrien@example.com] [accID: 11]", + ], + }, + "searching": { + "hydratedPersonalDetails": [ + "1 Current User [alt: current.user@example.com] [accID: 1] [nav: current.user@example.com]", + "1003 Vít Novák [alt: vit.novak@example.com] [accID: 3]", + "1004 İsmail Yılmaz [alt: ismail@example.com] [accID: 4]", + "1006 Łukasz Wójcik [alt: lukasz@example.com] [accID: 6]", + "1007 Οδυσσεύς [alt: odysseus@example.com] [accID: 7]", + "1009 Ana​Maria [alt: ana.maria@example.com] [accID: 9]", + "101 Bea Ames [alt: bea.ames.1@example.com] [accID: 101] [nav: bea.ames.1@example.com]", + "1010 Bob Builder [alt: bob.builder@example.com] [accID: 10]", + "1012 first.last+tag@x.com [alt: first.last+tag@x.com] [accID: 12]", + "1013 (500) 555-0006 [alt: +15005550006@expensify.sms] [accID: 13]", + "1015 Alex Smith Jr. [alt: alex.smith2@example.com] [accID: 15]", + "1016 Expensify Notifications [alt: notifications@expensify.com] [accID: 11665625]", + "1018 Domain Admin [alt: admin@example.com] [accID: 18]", + "1019 No Login [accID: 19]", + "1021 Concierge [alt: concierge@expensify.com] [accID: -1]", + "104 Eve Ames [alt: eve.ames.4@example.com] [accID: 104] [nav: eve.ames.4@example.com]", + "107 Hal Ames [alt: hal.ames.7@example.com] [accID: 107] [nav: hal.ames.7@example.com]", + "110 Kim Ames [alt: kim.ames.10@example.com] [accID: 110] [nav: kim.ames.10@example.com]", + "1100 Ada Ames [alt: ada.ames.0@example.com] [accID: 100]", + "1102 Cyd Ames [alt: cyd.ames.2@example.com] [accID: 102]", + "1103 Dan Ames [alt: dan.ames.3@example.com] [accID: 103]", + "1105 Fay Ames [alt: fay.ames.5@example.com] [accID: 105]", + "1106 Gus Ames [alt: gus.ames.6@example.com] [accID: 106]", + "1108 Ivy Ames [alt: ivy.ames.8@example.com] [accID: 108]", + "1109 Jon Ames [alt: jon.ames.9@example.com] [accID: 109]", + "1111 Lou Ames [alt: lou.ames.11@example.com] [accID: 111]", + "1112 Ada Byrd [alt: ada.byrd.12@example.com] [accID: 112]", + "1114 Cyd Byrd [alt: cyd.byrd.14@example.com] [accID: 114]", + "1115 Dan Byrd [alt: dan.byrd.15@example.com] [accID: 115]", + "1117 Fay Byrd [alt: fay.byrd.17@example.com] [accID: 117]", + "1118 Gus Byrd [alt: gus.byrd.18@example.com] [accID: 118]", + "1120 Ivy Byrd [alt: ivy.byrd.20@example.com] [accID: 120]", + "1121 Jon Byrd [alt: jon.byrd.21@example.com] [accID: 121]", + "1123 Lou Byrd [alt: lou.byrd.23@example.com] [accID: 123]", + "1124 Ada Cole [alt: ada.cole.24@example.com] [accID: 124]", + "1126 Cyd Cole [alt: cyd.cole.26@example.com] [accID: 126]", + "1127 Dan Cole [alt: dan.cole.27@example.com] [accID: 127]", + "1129 Fay Cole [alt: fay.cole.29@example.com] [accID: 129]", + "113 Bea Byrd [alt: bea.byrd.13@example.com] [accID: 113] [nav: bea.byrd.13@example.com]", + "1130 Gus Cole [alt: gus.cole.30@example.com] [accID: 130]", + "1132 Ivy Cole [alt: ivy.cole.32@example.com] [accID: 132]", + "1133 Jon Cole [alt: jon.cole.33@example.com] [accID: 133]", + "1135 Lou Cole [alt: lou.cole.35@example.com] [accID: 135]", + "1136 Ada Dunn [alt: ada.dunn.36@example.com] [accID: 136]", + "1138 Cyd Dunn [alt: cyd.dunn.38@example.com] [accID: 138]", + "1139 Dan Dunn [alt: dan.dunn.39@example.com] [accID: 139]", + "116 Eve Byrd [alt: eve.byrd.16@example.com] [accID: 116] [nav: eve.byrd.16@example.com]", + "119 Hal Byrd [alt: hal.byrd.19@example.com] [accID: 119] [nav: hal.byrd.19@example.com]", + "122 Kim Byrd [alt: kim.byrd.22@example.com] [accID: 122] [nav: kim.byrd.22@example.com]", + "125 Bea Cole [alt: bea.cole.25@example.com] [accID: 125] [nav: bea.cole.25@example.com]", + "128 Eve Cole [alt: eve.cole.28@example.com] [accID: 128] [nav: eve.cole.28@example.com]", + "131 Hal Cole [alt: hal.cole.31@example.com] [accID: 131] [nav: hal.cole.31@example.com]", + "134 Kim Cole [alt: kim.cole.34@example.com] [accID: 134] [nav: kim.cole.34@example.com]", + "137 Bea Dunn [alt: bea.dunn.37@example.com] [accID: 137] [nav: bea.dunn.37@example.com]", + "17 Account Executive [alt: Account Executive] [accID: 17] [nav: Account Executive]", + "20 René Dupont [alt: rene@example.com] [accID: 20] [nav: rene@example.com]", + "2000 Mary-Jane O'Brien [archived] [alt: obrien@example.com] [accID: 11]", + "2001 Alex Smith [alt: alex.smith@example.com] [accID: 14]", + "2020 José Ñúñez [alt: jose.nunez@example.com] [accID: 2]", + "5 Groß Straße [alt: strasse@example.com] [accID: 5] [nav: strasse@example.com]", + "8 Zoë 👩‍💻 [alt: zoe@example.com] [accID: 8] [nav: zoe@example.com]", + ], + "personalDetails": [ + " | 1 | current.user@example.com", + " | 101 | bea.ames.1@example.com", + " | 104 | eve.ames.4@example.com", + " | 107 | hal.ames.7@example.com", + " | 110 | kim.ames.10@example.com", + " | 113 | bea.byrd.13@example.com", + " | 116 | eve.byrd.16@example.com", + " | 119 | hal.byrd.19@example.com", + " | 122 | kim.byrd.22@example.com", + " | 125 | bea.cole.25@example.com", + " | 128 | eve.cole.28@example.com", + " | 131 | hal.cole.31@example.com", + " | 134 | kim.cole.34@example.com", + " | 137 | bea.dunn.37@example.com", + " | 17 | Account Executive", + " | 20 | rene@example.com", + " | 5 | strasse@example.com", + " | 8 | zoe@example.com", + "1003 | 3 | vit.novak@example.com", + "1004 | 4 | ismail@example.com", + "1006 | 6 | lukasz@example.com", + "1007 | 7 | odysseus@example.com", + "1009 | 9 | ana.maria@example.com", + "1010 | 10 | bob.builder@example.com", + "1012 | 12 | first.last+tag@x.com", + "1013 | 13 | +15005550006@expensify.sms", + "1015 | 15 | alex.smith2@example.com", + "1016 | 11665625 | notifications@expensify.com", + "1018 | 18 | admin@example.com", + "1019 | 19 | ", + "1021 | -1 | concierge@expensify.com", + "1100 | 100 | ada.ames.0@example.com", + "1102 | 102 | cyd.ames.2@example.com", + "1103 | 103 | dan.ames.3@example.com", + "1105 | 105 | fay.ames.5@example.com", + "1106 | 106 | gus.ames.6@example.com", + "1108 | 108 | ivy.ames.8@example.com", + "1109 | 109 | jon.ames.9@example.com", + "1111 | 111 | lou.ames.11@example.com", + "1112 | 112 | ada.byrd.12@example.com", + "1114 | 114 | cyd.byrd.14@example.com", + "1115 | 115 | dan.byrd.15@example.com", + "1117 | 117 | fay.byrd.17@example.com", + "1118 | 118 | gus.byrd.18@example.com", + "1120 | 120 | ivy.byrd.20@example.com", + "1121 | 121 | jon.byrd.21@example.com", + "1123 | 123 | lou.byrd.23@example.com", + "1124 | 124 | ada.cole.24@example.com", + "1126 | 126 | cyd.cole.26@example.com", + "1127 | 127 | dan.cole.27@example.com", + "1129 | 129 | fay.cole.29@example.com", + "1130 | 130 | gus.cole.30@example.com", + "1132 | 132 | ivy.cole.32@example.com", + "1133 | 133 | jon.cole.33@example.com", + "1135 | 135 | lou.cole.35@example.com", + "1136 | 136 | ada.dunn.36@example.com", + "1138 | 138 | cyd.dunn.38@example.com", + "1139 | 139 | dan.dunn.39@example.com", + "2000 | 11 | obrien@example.com", + "2001 | 14 | alex.smith@example.com", + "2020 | 2 | jose.nunez@example.com", + ], + "reports": [ + "1003 Vít Novák [alt: vit.novak@example.com] [accID: 3]", + "1004 İsmail Yılmaz [alt: ismail@example.com] [accID: 4]", + "1006 Łukasz Wójcik [alt: lukasz@example.com] [accID: 6]", + "1007 Οδυσσεύς [alt: odysseus@example.com] [accID: 7]", + "1009 Ana​Maria [alt: ana.maria@example.com] [accID: 9]", + "1010 Bob Builder [alt: bob.builder@example.com] [accID: 10]", + "1012 first.last+tag@x.com [alt: first.last+tag@x.com] [accID: 12]", + "1013 (500) 555-0006 [alt: (500) 555-0006] [accID: 13]", + "1015 Alex Smith Jr. [alt: alex.smith2@example.com] [accID: 15]", + "1016 Expensify Notifications [alt: notifications@expensify.com] [accID: 11665625]", + "1018 Domain Admin [alt: admin@example.com] [accID: 18]", + "1019 No Login [accID: 19]", + "1021 Concierge [alt: concierge@expensify.com] [accID: -1]", + "1100 Ada Ames [alt: ada.ames.0@example.com] [accID: 100]", + "1102 Cyd Ames [alt: cyd.ames.2@example.com] [accID: 102]", + "1103 Dan Ames [alt: dan.ames.3@example.com] [accID: 103]", + "1105 Fay Ames [alt: fay.ames.5@example.com] [accID: 105]", + "1106 Gus Ames [alt: gus.ames.6@example.com] [accID: 106]", + "1108 Ivy Ames [alt: ivy.ames.8@example.com] [accID: 108]", + "1109 Jon Ames [alt: jon.ames.9@example.com] [accID: 109]", + "1111 Lou Ames [alt: lou.ames.11@example.com] [accID: 111]", + "1112 Ada Byrd [alt: ada.byrd.12@example.com] [accID: 112]", + "1114 Cyd Byrd [alt: cyd.byrd.14@example.com] [accID: 114]", + "1115 Dan Byrd [alt: dan.byrd.15@example.com] [accID: 115]", + "1117 Fay Byrd [alt: fay.byrd.17@example.com] [accID: 117]", + "1118 Gus Byrd [alt: gus.byrd.18@example.com] [accID: 118]", + "1120 Ivy Byrd [alt: ivy.byrd.20@example.com] [accID: 120]", + "1121 Jon Byrd [alt: jon.byrd.21@example.com] [accID: 121]", + "1123 Lou Byrd [alt: lou.byrd.23@example.com] [accID: 123]", + "1124 Ada Cole [alt: ada.cole.24@example.com] [accID: 124]", + "1126 Cyd Cole [alt: cyd.cole.26@example.com] [accID: 126]", + "1127 Dan Cole [alt: dan.cole.27@example.com] [accID: 127]", + "1129 Fay Cole [alt: fay.cole.29@example.com] [accID: 129]", + "1130 Gus Cole [alt: gus.cole.30@example.com] [accID: 130]", + "1132 Ivy Cole [alt: ivy.cole.32@example.com] [accID: 132]", + "1133 Jon Cole [alt: jon.cole.33@example.com] [accID: 133]", + "1135 Lou Cole [alt: lou.cole.35@example.com] [accID: 135]", + "1136 Ada Dunn [alt: ada.dunn.36@example.com] [accID: 136]", + "1138 Cyd Dunn [alt: cyd.dunn.38@example.com] [accID: 138]", + "1139 Dan Dunn [alt: dan.dunn.39@example.com] [accID: 139]", + "2000 Mary-Jane O'Brien (archived) [archived] [alt: obrien@example.com] [accID: 11]", + "2001 Alex Smith [alt: alex.smith@example.com] [accID: 14]", + "2002 [accID: 999]", + "2003 Weekend Hiking [alt: Group]", + "2004 Current User (you) [alt: current.user@example.com] [accID: 1]", + "2005 Current User's expenses [gbr] [sub: Vít Workspace • Submits to Current User]", + "2006 Groß Straße's expenses [sub: Vít Workspace]", + "2007 Groß Straße [sub: Invoices]", + "2010 #admins [sub: Vít Workspace]", + "2011 #announce [sub: Vít Workspace]", + "2012 #vít-room [rbr] [sub: Acme Holdings]", + "2013 #orphan-room [sub: Gone Workspace]", + "2020 José Ñúñez [alt: jose.nunez@example.com] [accID: 2]", + "2021 Thread about the offsite [alt: Thread]", + "2022 Hidden thread [alt: Thread]", + "2023 Empty thread [alt: Thread]", + "2024 Empty thread with a draft [alt: Thread]", + "2025 Empty pinned thread [alt: Thread]", + "2026 Packing list [alt: Thread]", + "2030 Book flights [alt: Task]", + "2031 Offsite expenses [alt: Expense] [accID: 1]", + "2032 Empty submitted report [alt: Expense] [accID: 1]", + "2034 José Ñúñez [sub: Vít Workspace]", + ], + }, +} +`; + +exports[`OptionsListUtils search parity (golden master) call site searchRouterDisplayed matches the golden master for every query 1`] = ` +{ + "": { + "displayedRows": [ + "2004 Current User (you) [alt: This is your personal space. Use it for notes, tasks, drafts, and reminders.] [accID: 1]", + "2013 #orphan-room [alt: Gone Workspace • This chat room is for anything #orphan-room related.] [sub: Gone Workspace]", + "1124 Ada Cole [alt: This chat is with Ada Cole.] [accID: 124]", + "1103 Dan Ames [alt: This chat is with Dan Ames.] [accID: 103]", + "1019 No Login [alt: This chat is with No Login.] [accID: 19]", + "1123 Lou Byrd [alt: This chat is with Lou Byrd.] [accID: 123]", + "1102 Cyd Ames [alt: This chat is with Cyd Ames.] [accID: 102]", + "1018 Domain Admin [alt: This chat is with Domain Admin.] [accID: 18]", + "1121 Jon Byrd [alt: This chat is with Jon Byrd.] [accID: 121]", + "1100 Ada Ames [alt: This chat is with Ada Ames.] [accID: 100]", + "1120 Ivy Byrd [alt: This chat is with Ivy Byrd.] [accID: 120]", + "1015 Alex Smith Jr. [alt: This chat is with Alex Smith Jr..] [accID: 15]", + "2012 #vít-room [rbr] [alt: Acme Holdings • This chat room is for anything #vít-room related.] [sub: Acme Holdings]", + "1139 Dan Dunn [alt: This chat is with Dan Dunn.] [accID: 139]", + "1118 Gus Byrd [alt: This chat is with Gus Byrd.] [accID: 118]", + "1013 (500) 555-0006 [alt: This chat is with (500) 555-0006.] [accID: 13]", + "2007 Groß Straße [alt: Vít Workspace • This chat is for invoices between Groß Straße and Vít Workspace. Use the + button to send an invoice.] [sub: Invoices]", + "1138 Cyd Dunn [alt: This chat is with Cyd Dunn.] [accID: 138]", + "1117 Fay Byrd [alt: This chat is with Fay Byrd.] [accID: 117]", + "1012 first.last+tag@x.com [alt: This chat is with first.last+tag@x.com.] [accID: 12]", + ], + "hasMore": true, + }, + " ": { + "displayedRows": [ + "2004 Current User (you) [alt: This is your personal space. Use it for notes, tasks, drafts, and reminders.] [accID: 1]", + "2013 #orphan-room [alt: Gone Workspace • This chat room is for anything #orphan-room related.] [sub: Gone Workspace]", + "1124 Ada Cole [alt: This chat is with Ada Cole.] [accID: 124]", + "1103 Dan Ames [alt: This chat is with Dan Ames.] [accID: 103]", + "1019 No Login [alt: This chat is with No Login.] [accID: 19]", + "1123 Lou Byrd [alt: This chat is with Lou Byrd.] [accID: 123]", + "1102 Cyd Ames [alt: This chat is with Cyd Ames.] [accID: 102]", + "1018 Domain Admin [alt: This chat is with Domain Admin.] [accID: 18]", + "1121 Jon Byrd [alt: This chat is with Jon Byrd.] [accID: 121]", + "1100 Ada Ames [alt: This chat is with Ada Ames.] [accID: 100]", + "1120 Ivy Byrd [alt: This chat is with Ivy Byrd.] [accID: 120]", + "1015 Alex Smith Jr. [alt: This chat is with Alex Smith Jr..] [accID: 15]", + "2012 #vít-room [rbr] [alt: Acme Holdings • This chat room is for anything #vít-room related.] [sub: Acme Holdings]", + "1139 Dan Dunn [alt: This chat is with Dan Dunn.] [accID: 139]", + "1118 Gus Byrd [alt: This chat is with Gus Byrd.] [accID: 118]", + "1013 (500) 555-0006 [alt: This chat is with (500) 555-0006.] [accID: 13]", + "2007 Groß Straße [alt: Vít Workspace • This chat is for invoices between Groß Straße and Vít Workspace. Use the + button to send an invoice.] [sub: Invoices]", + "1138 Cyd Dunn [alt: This chat is with Cyd Dunn.] [accID: 138]", + "1117 Fay Byrd [alt: This chat is with Fay Byrd.] [accID: 117]", + "1012 first.last+tag@x.com [alt: This chat is with first.last+tag@x.com.] [accID: 12]", + ], + "hasMore": true, + }, + " alex.smith@example.com ": { + "displayedRows": [ + "2001 Alex Smith [alt: alex.smith@example.com] [accID: 14]", + ], + "hasMore": false, + }, + "(500) 555": { + "displayedRows": [ + "1013 (500) 555-0006 [alt: This chat is with (500) 555-0006.] [accID: 13]", + ], + "hasMore": false, + }, + "*": { + "displayedRows": [], + "hasMore": false, + }, + "+1 650 555 0100": { + "displayedRows": [ + "1100537387 +1 650 555 0100 [alt: +1 650 555 0100] [accID: 1100537387] [nav: +1 650 555 0100]", + ], + "hasMore": false, + }, + "+15005550006": { + "displayedRows": [ + "1013 (500) 555-0006 [alt: This chat is with (500) 555-0006.] [accID: 13]", + ], + "hasMore": false, + }, + ".*": { + "displayedRows": [], + "hasMore": false, + }, + "5550006": { + "displayedRows": [ + "1013 (500) 555-0006 [alt: This chat is with (500) 555-0006.] [accID: 13]", + ], + "hasMore": false, + }, + "@example": { + "displayedRows": [ + "2004 Current User (you) [alt: This is your personal space. Use it for notes, tasks, drafts, and reminders.] [accID: 1]", + "2001 Alex Smith [alt: alex.smith@example.com] [accID: 14]", + "1124 Ada Cole [alt: This chat is with Ada Cole.] [accID: 124]", + "1103 Dan Ames [alt: This chat is with Dan Ames.] [accID: 103]", + "1123 Lou Byrd [alt: This chat is with Lou Byrd.] [accID: 123]", + "1102 Cyd Ames [alt: This chat is with Cyd Ames.] [accID: 102]", + "1018 Domain Admin [alt: This chat is with Domain Admin.] [accID: 18]", + "1121 Jon Byrd [alt: This chat is with Jon Byrd.] [accID: 121]", + "1100 Ada Ames [alt: This chat is with Ada Ames.] [accID: 100]", + "1120 Ivy Byrd [alt: This chat is with Ivy Byrd.] [accID: 120]", + "1015 Alex Smith Jr. [alt: This chat is with Alex Smith Jr..] [accID: 15]", + "1139 Dan Dunn [alt: This chat is with Dan Dunn.] [accID: 139]", + "1118 Gus Byrd [alt: This chat is with Gus Byrd.] [accID: 118]", + "1138 Cyd Dunn [alt: This chat is with Cyd Dunn.] [accID: 138]", + "1117 Fay Byrd [alt: This chat is with Fay Byrd.] [accID: 117]", + "2031 Offsite expenses [alt: No activity yet] [accID: 1]", + "1136 Ada Dunn [alt: This chat is with Ada Dunn.] [accID: 136]", + "1115 Dan Byrd [alt: This chat is with Dan Byrd.] [accID: 115]", + "1010 Bob Builder [bold] [alt: See you at the trailhead] [accID: 10]", + "1135 Lou Cole [alt: This chat is with Lou Cole.] [accID: 135]", + ], + "hasMore": true, + }, + "CURRENT.USER@EXAMPLE.COM": { + "displayedRows": [ + "2004 Current User (you) [alt: This is your personal space. Use it for notes, tasks, drafts, and reminders.] [accID: 1]", + "2031 Offsite expenses [alt: No activity yet] [accID: 1]", + "2003 Weekend Hiking [alt: This chat is with José Ñúñez, Mary-Jane O'Brien, and Vít Novák.]", + ], + "hasMore": false, + }, + "Groß": { + "displayedRows": [ + "5 Groß Straße [alt: strasse@example.com] [accID: 5] [nav: strasse@example.com]", + "2006 Groß Straße's expenses [alt: Vít Workspace • This is where Groß Straße will submit expenses to Vít Workspace. Just use the + button.] [sub: Vít Workspace]", + "2007 Groß Straße [alt: Vít Workspace • This chat is for invoices between Groß Straße and Vít Workspace. Use the + button to send an invoice.] [sub: Invoices]", + ], + "hasMore": false, + }, + "JOSÉ": { + "displayedRows": [ + "2020 José Ñúñez [bold] [alt: Packing list] [accID: 2]", + "2003 Weekend Hiking [alt: This chat is with José Ñúñez, Mary-Jane O'Brien, and Vít Novák.]", + ], + "hasMore": false, + }, + "René": { + "displayedRows": [ + "20 René Dupont [alt: rene@example.com] [accID: 20] [nav: rene@example.com]", + ], + "hasMore": false, + }, + "Vít": { + "displayedRows": [ + "1003 Vít Novák [alt: This chat is with Vít Novák.] [accID: 3]", + "2006 Groß Straße's expenses [alt: Vít Workspace • This is where Groß Straße will submit expenses to Vít Workspace. Just use the + button.] [sub: Vít Workspace]", + "2005 Current User's expenses [alt: Vít Workspace • This is where Current User will submit expenses to Vít Workspace. Just use the + button.] [sub: Vít Workspace • Submits to Current User]", + "2003 Weekend Hiking [alt: This chat is with José Ñúñez, Mary-Jane O'Brien, and Vít Novák.]", + "2012 #vít-room [rbr] [alt: Acme Holdings • This chat room is for anything #vít-room related.] [sub: Acme Holdings]", + "2011 #announce [alt: Vít Workspace • This chat is with everyone in Vít Workspace. Use it for the most important announcements.] [sub: Vít Workspace]", + "2010 #admins [alt: Vít Workspace • This chat is with Vít Workspace admin. Use it to chat about workspace setup and more.] [sub: Vít Workspace]", + ], + "hasMore": false, + }, + "[": { + "displayedRows": [], + "hasMore": false, + }, + "\\": { + "displayedRows": [], + "hasMore": false, + }, + "a": { + "displayedRows": [ + "2004 Current User (you) [alt: This is your personal space. Use it for notes, tasks, drafts, and reminders.] [accID: 1]", + "2001 Alex Smith [alt: alex.smith@example.com] [accID: 14]", + "1124 Ada Cole [alt: This chat is with Ada Cole.] [accID: 124]", + "1103 Dan Ames [alt: This chat is with Dan Ames.] [accID: 103]", + "1123 Lou Byrd [alt: This chat is with Lou Byrd.] [accID: 123]", + "1102 Cyd Ames [alt: This chat is with Cyd Ames.] [accID: 102]", + "1018 Domain Admin [alt: This chat is with Domain Admin.] [accID: 18]", + "1121 Jon Byrd [alt: This chat is with Jon Byrd.] [accID: 121]", + "1100 Ada Ames [alt: This chat is with Ada Ames.] [accID: 100]", + "1120 Ivy Byrd [alt: This chat is with Ivy Byrd.] [accID: 120]", + "1015 Alex Smith Jr. [alt: This chat is with Alex Smith Jr..] [accID: 15]", + "1139 Dan Dunn [alt: This chat is with Dan Dunn.] [accID: 139]", + "1118 Gus Byrd [alt: This chat is with Gus Byrd.] [accID: 118]", + "1138 Cyd Dunn [alt: This chat is with Cyd Dunn.] [accID: 138]", + "1117 Fay Byrd [alt: This chat is with Fay Byrd.] [accID: 117]", + "1012 first.last+tag@x.com [alt: This chat is with first.last+tag@x.com.] [accID: 12]", + "2031 Offsite expenses [alt: No activity yet] [accID: 1]", + "1136 Ada Dunn [alt: This chat is with Ada Dunn.] [accID: 136]", + "1112 Ada Byrd [alt: ada.byrd.12@example.com] [accID: 112]", + "2013 #orphan-room [alt: Gone Workspace • This chat room is for anything #orphan-room related.] [sub: Gone Workspace]", + ], + "hasMore": true, + }, + "acme": { + "displayedRows": [ + "2012 #vít-room [rbr] [alt: Acme Holdings • This chat room is for anything #vít-room related.] [sub: Acme Holdings]", + ], + "hasMore": false, + }, + "ada byrd": { + "displayedRows": [ + "1112 Ada Byrd [alt: This chat is with Ada Byrd.] [accID: 112]", + ], + "hasMore": false, + }, + "admins": { + "displayedRows": [ + "2010 #admins [alt: Vít Workspace • This chat is with Vít Workspace admin. Use it to chat about workspace setup and more.] [sub: Vít Workspace]", + ], + "hasMore": false, + }, + "al": { + "displayedRows": [ + "2001 Alex Smith [alt: alex.smith@example.com] [accID: 14]", + "1015 Alex Smith Jr. [alt: This chat is with Alex Smith Jr..] [accID: 15]", + "107 Hal Ames [alt: hal.ames.7@example.com] [accID: 107] [nav: hal.ames.7@example.com]", + "119 Hal Byrd [alt: hal.byrd.19@example.com] [accID: 119] [nav: hal.byrd.19@example.com]", + "131 Hal Cole [alt: hal.cole.31@example.com] [accID: 131] [nav: hal.cole.31@example.com]", + ], + "hasMore": false, + }, + "alex smith": { + "displayedRows": [ + "2001 Alex Smith [alt: alex.smith@example.com] [accID: 14]", + "1015 Alex Smith Jr. [alt: This chat is with Alex Smith Jr..] [accID: 15]", + ], + "hasMore": false, + }, + "anamaria": { + "displayedRows": [ + "1009 Ana​Maria [alt: This chat is with Ana​Maria.] [accID: 9]", + ], + "hasMore": false, + }, + "bob builder": { + "displayedRows": [ + "1010 Bob Builder [bold] [alt: See you at the trailhead] [accID: 10]", + ], + "hasMore": false, + }, + "cole lou": { + "displayedRows": [ + "1135 Lou Cole [alt: This chat is with Lou Cole.] [accID: 135]", + ], + "hasMore": false, + }, + "concierge": { + "displayedRows": [ + "1021 Concierge [alt: Concierge can answer questions, update expenses, and more.] [accID: -1]", + ], + "hasMore": false, + }, + "ex sm": { + "displayedRows": [ + "2001 Alex Smith [alt: alex.smith@example.com] [accID: 14]", + "1015 Alex Smith Jr. [alt: This chat is with Alex Smith Jr..] [accID: 15]", + "1013 (500) 555-0006 [alt: This chat is with (500) 555-0006.] [accID: 13]", + "1004 İsmail Yılmaz [alt: Thanks!] [accID: 4]", + ], + "hasMore": false, + }, + "example.com": { + "displayedRows": [ + "2004 Current User (you) [alt: This is your personal space. Use it for notes, tasks, drafts, and reminders.] [accID: 1]", + "2001 Alex Smith [alt: alex.smith@example.com] [accID: 14]", + "1124 Ada Cole [alt: This chat is with Ada Cole.] [accID: 124]", + "1103 Dan Ames [alt: This chat is with Dan Ames.] [accID: 103]", + "1123 Lou Byrd [alt: This chat is with Lou Byrd.] [accID: 123]", + "1102 Cyd Ames [alt: This chat is with Cyd Ames.] [accID: 102]", + "1018 Domain Admin [alt: This chat is with Domain Admin.] [accID: 18]", + "1121 Jon Byrd [alt: This chat is with Jon Byrd.] [accID: 121]", + "1100 Ada Ames [alt: This chat is with Ada Ames.] [accID: 100]", + "1120 Ivy Byrd [alt: This chat is with Ivy Byrd.] [accID: 120]", + "1015 Alex Smith Jr. [alt: This chat is with Alex Smith Jr..] [accID: 15]", + "1139 Dan Dunn [alt: This chat is with Dan Dunn.] [accID: 139]", + "1118 Gus Byrd [alt: This chat is with Gus Byrd.] [accID: 118]", + "1138 Cyd Dunn [alt: This chat is with Cyd Dunn.] [accID: 138]", + "1117 Fay Byrd [alt: This chat is with Fay Byrd.] [accID: 117]", + "2031 Offsite expenses [alt: No activity yet] [accID: 1]", + "1136 Ada Dunn [alt: This chat is with Ada Dunn.] [accID: 136]", + "1115 Dan Byrd [alt: This chat is with Dan Byrd.] [accID: 115]", + "1010 Bob Builder [bold] [alt: See you at the trailhead] [accID: 10]", + "1135 Lou Cole [alt: This chat is with Lou Cole.] [accID: 135]", + ], + "hasMore": true, + }, + "firstlast": { + "displayedRows": [ + "1012 first.last+tag@x.com [alt: This chat is with first.last+tag@x.com.] [accID: 12]", + ], + "hasMore": false, + }, + "hiking": { + "displayedRows": [ + "2003 Weekend Hiking [alt: This chat is with José Ñúñez, Mary-Jane O'Brien, and Vít Novák.]", + ], + "hasMore": false, + }, + "ikin": { + "displayedRows": [ + "2003 Weekend Hiking [alt: This chat is with José Ñúñez, Mary-Jane O'Brien, and Vít Novák.]", + ], + "hasMore": false, + }, + "ismail": { + "displayedRows": [ + "1004 İsmail Yılmaz [alt: Thanks!] [accID: 4]", + ], + "hasMore": false, + }, + "jr alex": { + "displayedRows": [ + "1015 Alex Smith Jr. [alt: This chat is with Alex Smith Jr..] [accID: 15]", + ], + "hasMore": false, + }, + "last+tag": { + "displayedRows": [ + "1012 first.last+tag@x.com [alt: This chat is with first.last+tag@x.com.] [accID: 12]", + ], + "hasMore": false, + }, + "lukasz": { + "displayedRows": [ + "1006 Łukasz Wójcik [alt: This chat is with Łukasz Wójcik.] [accID: 6]", + ], + "hasMore": false, + }, + "mary-jane": { + "displayedRows": [ + "2003 Weekend Hiking [alt: This chat is with José Ñúñez, Mary-Jane O'Brien, and Vít Novák.]", + "2000 Mary-Jane O'Brien (archived) [archived] [alt: This chat room has been archived.] [accID: 11]", + ], + "hasMore": false, + }, + "me": { + "displayedRows": [ + "2004 Current User (you) [alt: This is your personal space. Use it for notes, tasks, drafts, and reminders.] [accID: 1]", + "1103 Dan Ames [alt: This chat is with Dan Ames.] [accID: 103]", + "1102 Cyd Ames [alt: This chat is with Cyd Ames.] [accID: 102]", + "1100 Ada Ames [alt: This chat is with Ada Ames.] [accID: 100]", + "1111 Lou Ames [alt: This chat is with Lou Ames.] [accID: 111]", + "1109 Jon Ames [alt: This chat is with Jon Ames.] [accID: 109]", + "1108 Ivy Ames [alt: This chat is with Ivy Ames.] [accID: 108]", + "1106 Gus Ames [alt: This chat is with Gus Ames.] [accID: 106]", + "1105 Fay Ames [alt: This chat is with Fay Ames.] [accID: 105]", + "101 Bea Ames [alt: bea.ames.1@example.com] [accID: 101] [nav: bea.ames.1@example.com]", + "104 Eve Ames [alt: eve.ames.4@example.com] [accID: 104] [nav: eve.ames.4@example.com]", + "107 Hal Ames [alt: hal.ames.7@example.com] [accID: 107] [nav: hal.ames.7@example.com]", + "2012 #vít-room [rbr] [alt: Acme Holdings • This chat room is for anything #vít-room related.] [sub: Acme Holdings]", + ], + "hasMore": true, + }, + "mith": { + "displayedRows": [ + "2001 Alex Smith [alt: alex.smith@example.com] [accID: 14]", + "1015 Alex Smith Jr. [alt: This chat is with Alex Smith Jr..] [accID: 15]", + ], + "hasMore": false, + }, + "new.person@example.com": { + "displayedRows": [ + "301623904 new.person@example.com [alt: new.person@example.com] [accID: 301623904] [nav: new.person@example.com]", + ], + "hasMore": false, + }, + "nounce": { + "displayedRows": [ + "2011 #announce [alt: Vít Workspace • This chat is with everyone in Vít Workspace. Use it for the most important announcements.] [sub: Vít Workspace]", + ], + "hasMore": false, + }, + "nunez": { + "displayedRows": [ + "2020 José Ñúñez [bold] [alt: Packing list] [accID: 2]", + "2003 Weekend Hiking [alt: This chat is with José Ñúñez, Mary-Jane O'Brien, and Vít Novák.]", + ], + "hasMore": false, + }, + "obrien": { + "displayedRows": [ + "2003 Weekend Hiking [alt: This chat is with José Ñúñez, Mary-Jane O'Brien, and Vít Novák.]", + "2000 Mary-Jane O'Brien (archived) [archived] [alt: This chat room has been archived.] [accID: 11]", + ], + "hasMore": false, + }, + "oldings": { + "displayedRows": [ + "2012 #vít-room [rbr] [alt: Acme Holdings • This chat room is for anything #vít-room related.] [sub: Acme Holdings]", + ], + "hasMore": false, + }, + "orkspace": { + "displayedRows": [ + "2006 Groß Straße's expenses [alt: Vít Workspace • This is where Groß Straße will submit expenses to Vít Workspace. Just use the + button.] [sub: Vít Workspace]", + "2005 Current User's expenses [alt: Vít Workspace • This is where Current User will submit expenses to Vít Workspace. Just use the + button.] [sub: Vít Workspace • Submits to Current User]", + "2013 #orphan-room [alt: Gone Workspace • This chat room is for anything #orphan-room related.] [sub: Gone Workspace]", + "2011 #announce [alt: Vít Workspace • This chat is with everyone in Vít Workspace. Use it for the most important announcements.] [sub: Vít Workspace]", + "2010 #admins [alt: Vít Workspace • This chat is with Vít Workspace admin. Use it to chat about workspace setup and more.] [sub: Vít Workspace]", + ], + "hasMore": false, + }, + "ovak": { + "displayedRows": [ + "1003 Vít Novák [alt: This chat is with Vít Novák.] [accID: 3]", + "2003 Weekend Hiking [alt: This chat is with José Ñúñez, Mary-Jane O'Brien, and Vít Novák.]", + ], + "hasMore": false, + }, + "packing thread": { + "displayedRows": [ + "2026 Packing list [bold] [alt: José Ñúñez: Tent and stove]", + ], + "hasMore": false, + }, + "rene": { + "displayedRows": [ + "20 René Dupont [alt: rene@example.com] [accID: 20] [nav: rene@example.com]", + ], + "hasMore": false, + }, + "smith jr": { + "displayedRows": [ + "1015 Alex Smith Jr. [alt: This chat is with Alex Smith Jr..] [accID: 15]", + ], + "hasMore": false, + }, + "strasse": { + "displayedRows": [ + "5 Groß Straße [alt: strasse@example.com] [accID: 5] [nav: strasse@example.com]", + "2006 Groß Straße's expenses [alt: Vít Workspace • This is where Groß Straße will submit expenses to Vít Workspace. Just use the + button.] [sub: Vít Workspace]", + "2007 Groß Straße [alt: Vít Workspace • This chat is for invoices between Groß Straße and Vít Workspace. Use the + button to send an invoice.] [sub: Invoices]", + ], + "hasMore": false, + }, + "thread": { + "displayedRows": [ + "2026 Packing list [bold] [alt: José Ñúñez: Tent and stove]", + "2025 Empty pinned thread [alt: No activity yet]", + "2024 Empty thread with a draft [alt: No activity yet]", + "2021 Thread about the offsite [bold] [alt: José Ñúñez: Count me in]", + ], + "hasMore": false, + }, + "to:al": { + "displayedRows": [], + "hasMore": false, + }, + "vit": { + "displayedRows": [ + "1003 Vít Novák [alt: This chat is with Vít Novák.] [accID: 3]", + "2006 Groß Straße's expenses [alt: Vít Workspace • This is where Groß Straße will submit expenses to Vít Workspace. Just use the + button.] [sub: Vít Workspace]", + "2005 Current User's expenses [alt: Vít Workspace • This is where Current User will submit expenses to Vít Workspace. Just use the + button.] [sub: Vít Workspace • Submits to Current User]", + "2003 Weekend Hiking [alt: This chat is with José Ñúñez, Mary-Jane O'Brien, and Vít Novák.]", + "2012 #vít-room [rbr] [alt: Acme Holdings • This chat room is for anything #vít-room related.] [sub: Acme Holdings]", + "2011 #announce [alt: Vít Workspace • This chat is with everyone in Vít Workspace. Use it for the most important announcements.] [sub: Vít Workspace]", + "2010 #admins [alt: Vít Workspace • This chat is with Vít Workspace admin. Use it to chat about workspace setup and more.] [sub: Vít Workspace]", + ], + "hasMore": false, + }, + "vit workspace": { + "displayedRows": [ + "2006 Groß Straße's expenses [alt: Vít Workspace • This is where Groß Straße will submit expenses to Vít Workspace. Just use the + button.] [sub: Vít Workspace]", + "2005 Current User's expenses [alt: Vít Workspace • This is where Current User will submit expenses to Vít Workspace. Just use the + button.] [sub: Vít Workspace • Submits to Current User]", + "2011 #announce [alt: Vít Workspace • This chat is with everyone in Vít Workspace. Use it for the most important announcements.] [sub: Vít Workspace]", + "2010 #admins [alt: Vít Workspace • This chat is with Vít Workspace admin. Use it to chat about workspace setup and more.] [sub: Vít Workspace]", + ], + "hasMore": false, + }, + "workspace": { + "displayedRows": [ + "2006 Groß Straße's expenses [alt: Vít Workspace • This is where Groß Straße will submit expenses to Vít Workspace. Just use the + button.] [sub: Vít Workspace]", + "2005 Current User's expenses [alt: Vít Workspace • This is where Current User will submit expenses to Vít Workspace. Just use the + button.] [sub: Vít Workspace • Submits to Current User]", + "2013 #orphan-room [alt: Gone Workspace • This chat room is for anything #orphan-room related.] [sub: Gone Workspace]", + "2011 #announce [alt: Vít Workspace • This chat is with everyone in Vít Workspace. Use it for the most important announcements.] [sub: Vít Workspace]", + "2010 #admins [alt: Vít Workspace • This chat is with Vít Workspace admin. Use it to chat about workspace setup and more.] [sub: Vít Workspace]", + ], + "hasMore": false, + }, + "x.com": { + "displayedRows": [ + "1012 first.last+tag@x.com [alt: This chat is with first.last+tag@x.com.] [accID: 12]", + ], + "hasMore": false, + }, + "you": { + "displayedRows": [ + "2004 Current User (you) [alt: This is your personal space. Use it for notes, tasks, drafts, and reminders.] [accID: 1]", + ], + "hasMore": false, + }, + "zzzqqq": { + "displayedRows": [], + "hasMore": false, + }, + "İSMAIL": { + "displayedRows": [ + "1004 İsmail Yılmaz [alt: Thanks!] [accID: 4]", + ], + "hasMore": false, + }, + "δυσσευς": { + "displayedRows": [ + "1007 Οδυσσεύς [alt: This chat is with Οδυσσεύς.] [accID: 7]", + ], + "hasMore": false, + }, + "δυσσευσ": { + "displayedRows": [], + "hasMore": false, + }, + "👩‍💻": { + "displayedRows": [ + "8 Zoë 👩‍💻 [alt: zoe@example.com] [accID: 8] [nav: zoe@example.com]", + ], + "hasMore": false, + }, +} +`; + +exports[`OptionsListUtils search parity (golden master) call site searchRouterLocal matches the golden master for every query 1`] = ` +{ + "": { + "currentUserOption": null, + "hasMore": true, + "personalDetails": [], + "recentReports": [ + "2004 Current User (you) [alt: This is your personal space. Use it for notes, tasks, drafts, and reminders.] [accID: 1]", + "2013 #orphan-room [alt: Gone Workspace • This chat room is for anything #orphan-room related.] [sub: Gone Workspace]", + "1124 Ada Cole [alt: This chat is with Ada Cole.] [accID: 124]", + "1103 Dan Ames [alt: This chat is with Dan Ames.] [accID: 103]", + "1019 No Login [alt: This chat is with No Login.] [accID: 19]", + "1123 Lou Byrd [alt: This chat is with Lou Byrd.] [accID: 123]", + "1102 Cyd Ames [alt: This chat is with Cyd Ames.] [accID: 102]", + "1018 Domain Admin [alt: This chat is with Domain Admin.] [accID: 18]", + "1121 Jon Byrd [alt: This chat is with Jon Byrd.] [accID: 121]", + "1100 Ada Ames [alt: This chat is with Ada Ames.] [accID: 100]", + "1120 Ivy Byrd [alt: This chat is with Ivy Byrd.] [accID: 120]", + "1015 Alex Smith Jr. [alt: This chat is with Alex Smith Jr..] [accID: 15]", + "2012 #vít-room [rbr] [alt: Acme Holdings • This chat room is for anything #vít-room related.] [sub: Acme Holdings]", + "1139 Dan Dunn [alt: This chat is with Dan Dunn.] [accID: 139]", + "1118 Gus Byrd [alt: This chat is with Gus Byrd.] [accID: 118]", + "1013 (500) 555-0006 [alt: This chat is with (500) 555-0006.] [accID: 13]", + "2007 Groß Straße [alt: Vít Workspace • This chat is for invoices between Groß Straße and Vít Workspace. Use the + button to send an invoice.] [sub: Invoices]", + "1138 Cyd Dunn [alt: This chat is with Cyd Dunn.] [accID: 138]", + "1117 Fay Byrd [alt: This chat is with Fay Byrd.] [accID: 117]", + "1012 first.last+tag@x.com [alt: This chat is with first.last+tag@x.com.] [accID: 12]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + " ": { + "currentUserOption": null, + "hasMore": true, + "personalDetails": [], + "recentReports": [ + "2004 Current User (you) [alt: This is your personal space. Use it for notes, tasks, drafts, and reminders.] [accID: 1]", + "2013 #orphan-room [alt: Gone Workspace • This chat room is for anything #orphan-room related.] [sub: Gone Workspace]", + "1124 Ada Cole [alt: This chat is with Ada Cole.] [accID: 124]", + "1103 Dan Ames [alt: This chat is with Dan Ames.] [accID: 103]", + "1019 No Login [alt: This chat is with No Login.] [accID: 19]", + "1123 Lou Byrd [alt: This chat is with Lou Byrd.] [accID: 123]", + "1102 Cyd Ames [alt: This chat is with Cyd Ames.] [accID: 102]", + "1018 Domain Admin [alt: This chat is with Domain Admin.] [accID: 18]", + "1121 Jon Byrd [alt: This chat is with Jon Byrd.] [accID: 121]", + "1100 Ada Ames [alt: This chat is with Ada Ames.] [accID: 100]", + "1120 Ivy Byrd [alt: This chat is with Ivy Byrd.] [accID: 120]", + "1015 Alex Smith Jr. [alt: This chat is with Alex Smith Jr..] [accID: 15]", + "2012 #vít-room [rbr] [alt: Acme Holdings • This chat room is for anything #vít-room related.] [sub: Acme Holdings]", + "1139 Dan Dunn [alt: This chat is with Dan Dunn.] [accID: 139]", + "1118 Gus Byrd [alt: This chat is with Gus Byrd.] [accID: 118]", + "1013 (500) 555-0006 [alt: This chat is with (500) 555-0006.] [accID: 13]", + "2007 Groß Straße [alt: Vít Workspace • This chat is for invoices between Groß Straße and Vít Workspace. Use the + button to send an invoice.] [sub: Invoices]", + "1138 Cyd Dunn [alt: This chat is with Cyd Dunn.] [accID: 138]", + "1117 Fay Byrd [alt: This chat is with Fay Byrd.] [accID: 117]", + "1012 first.last+tag@x.com [alt: This chat is with first.last+tag@x.com.] [accID: 12]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + " alex.smith@example.com ": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "2001 Alex Smith [alt: alex.smith@example.com] [accID: 14]", + ], + "recentReports": [], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "(500) 555": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "1013 (500) 555-0006 [alt: +15005550006@expensify.sms] [accID: 13]", + ], + "recentReports": [ + "1013 (500) 555-0006 [alt: This chat is with (500) 555-0006.] [accID: 13]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "*": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [], + "recentReports": [], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "+1 650 555 0100": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [], + "recentReports": [], + "selfDMChat": null, + "userToInvite": "1100537387 +1 650 555 0100 [alt: +1 650 555 0100] [accID: 1100537387]", + "workspaceChats": [], + }, + "+15005550006": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "1013 (500) 555-0006 [alt: +15005550006@expensify.sms] [accID: 13]", + ], + "recentReports": [ + "1013 (500) 555-0006 [alt: This chat is with (500) 555-0006.] [accID: 13]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + ".*": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [], + "recentReports": [], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "5550006": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "1013 (500) 555-0006 [alt: +15005550006@expensify.sms] [accID: 13]", + ], + "recentReports": [ + "1013 (500) 555-0006 [alt: This chat is with (500) 555-0006.] [accID: 13]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "@example": { + "currentUserOption": null, + "hasMore": true, + "personalDetails": [ + "1100 Ada Ames [alt: ada.ames.0@example.com] [accID: 100]", + "1112 Ada Byrd [alt: ada.byrd.12@example.com] [accID: 112]", + "1124 Ada Cole [alt: ada.cole.24@example.com] [accID: 124]", + "1136 Ada Dunn [alt: ada.dunn.36@example.com] [accID: 136]", + "2001 Alex Smith [alt: alex.smith@example.com] [accID: 14]", + ], + "recentReports": [ + "2004 Current User (you) [alt: This is your personal space. Use it for notes, tasks, drafts, and reminders.] [accID: 1]", + "1124 Ada Cole [alt: This chat is with Ada Cole.] [accID: 124]", + "1103 Dan Ames [alt: This chat is with Dan Ames.] [accID: 103]", + "1123 Lou Byrd [alt: This chat is with Lou Byrd.] [accID: 123]", + "1102 Cyd Ames [alt: This chat is with Cyd Ames.] [accID: 102]", + "1018 Domain Admin [alt: This chat is with Domain Admin.] [accID: 18]", + "1121 Jon Byrd [alt: This chat is with Jon Byrd.] [accID: 121]", + "1100 Ada Ames [alt: This chat is with Ada Ames.] [accID: 100]", + "1120 Ivy Byrd [alt: This chat is with Ivy Byrd.] [accID: 120]", + "1015 Alex Smith Jr. [alt: This chat is with Alex Smith Jr..] [accID: 15]", + "1139 Dan Dunn [alt: This chat is with Dan Dunn.] [accID: 139]", + "1118 Gus Byrd [alt: This chat is with Gus Byrd.] [accID: 118]", + "1138 Cyd Dunn [alt: This chat is with Cyd Dunn.] [accID: 138]", + "1117 Fay Byrd [alt: This chat is with Fay Byrd.] [accID: 117]", + "2031 Offsite expenses [alt: No activity yet] [accID: 1]", + "1136 Ada Dunn [alt: This chat is with Ada Dunn.] [accID: 136]", + "1115 Dan Byrd [alt: This chat is with Dan Byrd.] [accID: 115]", + "1010 Bob Builder [bold] [alt: See you at the trailhead] [accID: 10]", + "1135 Lou Cole [alt: This chat is with Lou Cole.] [accID: 135]", + "1114 Cyd Byrd [alt: This chat is with Cyd Byrd.] [accID: 114]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "CURRENT.USER@EXAMPLE.COM": { + "currentUserOption": "1 Current User [alt: current.user@example.com] [accID: 1] [nav: current.user@example.com]", + "hasMore": false, + "personalDetails": [ + "1 Current User [alt: current.user@example.com] [accID: 1] [nav: current.user@example.com]", + ], + "recentReports": [ + "2004 Current User (you) [alt: This is your personal space. Use it for notes, tasks, drafts, and reminders.] [accID: 1]", + "2031 Offsite expenses [alt: No activity yet] [accID: 1]", + "2003 Weekend Hiking [alt: This chat is with José Ñúñez, Mary-Jane O'Brien, and Vít Novák.]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "Groß": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "5 Groß Straße [alt: strasse@example.com] [accID: 5] [nav: strasse@example.com]", + ], + "recentReports": [ + "2007 Groß Straße [alt: Vít Workspace • This chat is for invoices between Groß Straße and Vít Workspace. Use the + button to send an invoice.] [sub: Invoices]", + "2006 Groß Straße's expenses [alt: Vít Workspace • This is where Groß Straße will submit expenses to Vít Workspace. Just use the + button.] [sub: Vít Workspace]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "JOSÉ": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "2020 José Ñúñez [alt: jose.nunez@example.com] [accID: 2]", + ], + "recentReports": [ + "2003 Weekend Hiking [alt: This chat is with José Ñúñez, Mary-Jane O'Brien, and Vít Novák.]", + "2020 José Ñúñez [bold] [alt: Packing list] [accID: 2]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "René": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "20 René Dupont [alt: rene@example.com] [accID: 20] [nav: rene@example.com]", + ], + "recentReports": [], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "Vít": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "1003 Vít Novák [alt: vit.novak@example.com] [accID: 3]", + ], + "recentReports": [ + "2012 #vít-room [rbr] [alt: Acme Holdings • This chat room is for anything #vít-room related.] [sub: Acme Holdings]", + "2011 #announce [alt: Vít Workspace • This chat is with everyone in Vít Workspace. Use it for the most important announcements.] [sub: Vít Workspace]", + "2006 Groß Straße's expenses [alt: Vít Workspace • This is where Groß Straße will submit expenses to Vít Workspace. Just use the + button.] [sub: Vít Workspace]", + "2005 Current User's expenses [alt: Vít Workspace • This is where Current User will submit expenses to Vít Workspace. Just use the + button.] [sub: Vít Workspace • Submits to Current User]", + "2003 Weekend Hiking [alt: This chat is with José Ñúñez, Mary-Jane O'Brien, and Vít Novák.]", + "2010 #admins [alt: Vít Workspace • This chat is with Vít Workspace admin. Use it to chat about workspace setup and more.] [sub: Vít Workspace]", + "1003 Vít Novák [alt: This chat is with Vít Novák.] [accID: 3]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "[": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [], + "recentReports": [], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "\\": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [], + "recentReports": [], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "a": { + "currentUserOption": null, + "hasMore": true, + "personalDetails": [ + "1100 Ada Ames [alt: ada.ames.0@example.com] [accID: 100]", + "1112 Ada Byrd [alt: ada.byrd.12@example.com] [accID: 112]", + "1124 Ada Cole [alt: ada.cole.24@example.com] [accID: 124]", + "1136 Ada Dunn [alt: ada.dunn.36@example.com] [accID: 136]", + "2001 Alex Smith [alt: alex.smith@example.com] [accID: 14]", + ], + "recentReports": [ + "2004 Current User (you) [alt: This is your personal space. Use it for notes, tasks, drafts, and reminders.] [accID: 1]", + "2013 #orphan-room [alt: Gone Workspace • This chat room is for anything #orphan-room related.] [sub: Gone Workspace]", + "1124 Ada Cole [alt: This chat is with Ada Cole.] [accID: 124]", + "1103 Dan Ames [alt: This chat is with Dan Ames.] [accID: 103]", + "1123 Lou Byrd [alt: This chat is with Lou Byrd.] [accID: 123]", + "1102 Cyd Ames [alt: This chat is with Cyd Ames.] [accID: 102]", + "1018 Domain Admin [alt: This chat is with Domain Admin.] [accID: 18]", + "1121 Jon Byrd [alt: This chat is with Jon Byrd.] [accID: 121]", + "1100 Ada Ames [alt: This chat is with Ada Ames.] [accID: 100]", + "1120 Ivy Byrd [alt: This chat is with Ivy Byrd.] [accID: 120]", + "1015 Alex Smith Jr. [alt: This chat is with Alex Smith Jr..] [accID: 15]", + "2012 #vít-room [rbr] [alt: Acme Holdings • This chat room is for anything #vít-room related.] [sub: Acme Holdings]", + "1139 Dan Dunn [alt: This chat is with Dan Dunn.] [accID: 139]", + "1118 Gus Byrd [alt: This chat is with Gus Byrd.] [accID: 118]", + "2007 Groß Straße [alt: Vít Workspace • This chat is for invoices between Groß Straße and Vít Workspace. Use the + button to send an invoice.] [sub: Invoices]", + "1138 Cyd Dunn [alt: This chat is with Cyd Dunn.] [accID: 138]", + "1117 Fay Byrd [alt: This chat is with Fay Byrd.] [accID: 117]", + "1012 first.last+tag@x.com [alt: This chat is with first.last+tag@x.com.] [accID: 12]", + "2031 Offsite expenses [alt: No activity yet] [accID: 1]", + "1136 Ada Dunn [alt: This chat is with Ada Dunn.] [accID: 136]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "acme": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [], + "recentReports": [ + "2012 #vít-room [rbr] [alt: Acme Holdings • This chat room is for anything #vít-room related.] [sub: Acme Holdings]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "ada byrd": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "1112 Ada Byrd [alt: ada.byrd.12@example.com] [accID: 112]", + ], + "recentReports": [ + "1112 Ada Byrd [alt: This chat is with Ada Byrd.] [accID: 112]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "admins": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [], + "recentReports": [ + "2010 #admins [alt: Vít Workspace • This chat is with Vít Workspace admin. Use it to chat about workspace setup and more.] [sub: Vít Workspace]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "al": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "2001 Alex Smith [alt: alex.smith@example.com] [accID: 14]", + "1015 Alex Smith Jr. [alt: alex.smith2@example.com] [accID: 15]", + "107 Hal Ames [alt: hal.ames.7@example.com] [accID: 107] [nav: hal.ames.7@example.com]", + "119 Hal Byrd [alt: hal.byrd.19@example.com] [accID: 119] [nav: hal.byrd.19@example.com]", + "131 Hal Cole [alt: hal.cole.31@example.com] [accID: 131] [nav: hal.cole.31@example.com]", + ], + "recentReports": [ + "1015 Alex Smith Jr. [alt: This chat is with Alex Smith Jr..] [accID: 15]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "alex smith": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "2001 Alex Smith [alt: alex.smith@example.com] [accID: 14]", + "1015 Alex Smith Jr. [alt: alex.smith2@example.com] [accID: 15]", + ], + "recentReports": [ + "1015 Alex Smith Jr. [alt: This chat is with Alex Smith Jr..] [accID: 15]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "anamaria": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "1009 Ana​Maria [alt: ana.maria@example.com] [accID: 9]", + ], + "recentReports": [ + "1009 Ana​Maria [alt: This chat is with Ana​Maria.] [accID: 9]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "bob builder": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "1010 Bob Builder [alt: bob.builder@example.com] [accID: 10]", + ], + "recentReports": [ + "1010 Bob Builder [bold] [alt: See you at the trailhead] [accID: 10]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "cole lou": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "1135 Lou Cole [alt: lou.cole.35@example.com] [accID: 135]", + ], + "recentReports": [ + "1135 Lou Cole [alt: This chat is with Lou Cole.] [accID: 135]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "concierge": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "1021 Concierge [alt: concierge@expensify.com] [accID: -1]", + ], + "recentReports": [ + "1021 Concierge [alt: Concierge can answer questions, update expenses, and more.] [accID: -1]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "ex sm": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "1013 (500) 555-0006 [alt: +15005550006@expensify.sms] [accID: 13]", + "2001 Alex Smith [alt: alex.smith@example.com] [accID: 14]", + "1015 Alex Smith Jr. [alt: alex.smith2@example.com] [accID: 15]", + "1004 İsmail Yılmaz [alt: ismail@example.com] [accID: 4]", + ], + "recentReports": [ + "1015 Alex Smith Jr. [alt: This chat is with Alex Smith Jr..] [accID: 15]", + "1013 (500) 555-0006 [alt: This chat is with (500) 555-0006.] [accID: 13]", + "1004 İsmail Yılmaz [alt: Thanks!] [accID: 4]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "example.com": { + "currentUserOption": null, + "hasMore": true, + "personalDetails": [ + "1100 Ada Ames [alt: ada.ames.0@example.com] [accID: 100]", + "1112 Ada Byrd [alt: ada.byrd.12@example.com] [accID: 112]", + "1124 Ada Cole [alt: ada.cole.24@example.com] [accID: 124]", + "1136 Ada Dunn [alt: ada.dunn.36@example.com] [accID: 136]", + "2001 Alex Smith [alt: alex.smith@example.com] [accID: 14]", + ], + "recentReports": [ + "2004 Current User (you) [alt: This is your personal space. Use it for notes, tasks, drafts, and reminders.] [accID: 1]", + "1124 Ada Cole [alt: This chat is with Ada Cole.] [accID: 124]", + "1103 Dan Ames [alt: This chat is with Dan Ames.] [accID: 103]", + "1123 Lou Byrd [alt: This chat is with Lou Byrd.] [accID: 123]", + "1102 Cyd Ames [alt: This chat is with Cyd Ames.] [accID: 102]", + "1018 Domain Admin [alt: This chat is with Domain Admin.] [accID: 18]", + "1121 Jon Byrd [alt: This chat is with Jon Byrd.] [accID: 121]", + "1100 Ada Ames [alt: This chat is with Ada Ames.] [accID: 100]", + "1120 Ivy Byrd [alt: This chat is with Ivy Byrd.] [accID: 120]", + "1015 Alex Smith Jr. [alt: This chat is with Alex Smith Jr..] [accID: 15]", + "1139 Dan Dunn [alt: This chat is with Dan Dunn.] [accID: 139]", + "1118 Gus Byrd [alt: This chat is with Gus Byrd.] [accID: 118]", + "1138 Cyd Dunn [alt: This chat is with Cyd Dunn.] [accID: 138]", + "1117 Fay Byrd [alt: This chat is with Fay Byrd.] [accID: 117]", + "2031 Offsite expenses [alt: No activity yet] [accID: 1]", + "1136 Ada Dunn [alt: This chat is with Ada Dunn.] [accID: 136]", + "1115 Dan Byrd [alt: This chat is with Dan Byrd.] [accID: 115]", + "1010 Bob Builder [bold] [alt: See you at the trailhead] [accID: 10]", + "1135 Lou Cole [alt: This chat is with Lou Cole.] [accID: 135]", + "1114 Cyd Byrd [alt: This chat is with Cyd Byrd.] [accID: 114]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "firstlast": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "1012 first.last+tag@x.com [alt: first.last+tag@x.com] [accID: 12]", + ], + "recentReports": [ + "1012 first.last+tag@x.com [alt: This chat is with first.last+tag@x.com.] [accID: 12]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "hiking": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [], + "recentReports": [ + "2003 Weekend Hiking [alt: This chat is with José Ñúñez, Mary-Jane O'Brien, and Vít Novák.]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "ikin": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [], + "recentReports": [ + "2003 Weekend Hiking [alt: This chat is with José Ñúñez, Mary-Jane O'Brien, and Vít Novák.]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "ismail": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "1004 İsmail Yılmaz [alt: ismail@example.com] [accID: 4]", + ], + "recentReports": [ + "1004 İsmail Yılmaz [alt: Thanks!] [accID: 4]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "jr alex": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "1015 Alex Smith Jr. [alt: alex.smith2@example.com] [accID: 15]", + ], + "recentReports": [ + "1015 Alex Smith Jr. [alt: This chat is with Alex Smith Jr..] [accID: 15]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "last+tag": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "1012 first.last+tag@x.com [alt: first.last+tag@x.com] [accID: 12]", + ], + "recentReports": [ + "1012 first.last+tag@x.com [alt: This chat is with first.last+tag@x.com.] [accID: 12]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "lukasz": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "1006 Łukasz Wójcik [alt: lukasz@example.com] [accID: 6]", + ], + "recentReports": [ + "1006 Łukasz Wójcik [alt: This chat is with Łukasz Wójcik.] [accID: 6]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "mary-jane": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "2000 Mary-Jane O'Brien [archived] [alt: obrien@example.com] [accID: 11]", + ], + "recentReports": [ + "2003 Weekend Hiking [alt: This chat is with José Ñúñez, Mary-Jane O'Brien, and Vít Novák.]", + "2000 Mary-Jane O'Brien (archived) [archived] [alt: This chat room has been archived.] [accID: 11]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "me": { + "currentUserOption": "1 Current User [alt: current.user@example.com] [accID: 1] [nav: current.user@example.com]", + "hasMore": true, + "personalDetails": [ + "1100 Ada Ames [alt: ada.ames.0@example.com] [accID: 100]", + "101 Bea Ames [alt: bea.ames.1@example.com] [accID: 101] [nav: bea.ames.1@example.com]", + "1 Current User [alt: current.user@example.com] [accID: 1] [nav: current.user@example.com]", + "1102 Cyd Ames [alt: cyd.ames.2@example.com] [accID: 102]", + "1103 Dan Ames [alt: dan.ames.3@example.com] [accID: 103]", + "104 Eve Ames [alt: eve.ames.4@example.com] [accID: 104] [nav: eve.ames.4@example.com]", + "1105 Fay Ames [alt: fay.ames.5@example.com] [accID: 105]", + "1106 Gus Ames [alt: gus.ames.6@example.com] [accID: 106]", + "107 Hal Ames [alt: hal.ames.7@example.com] [accID: 107] [nav: hal.ames.7@example.com]", + ], + "recentReports": [ + "2004 Current User (you) [alt: This is your personal space. Use it for notes, tasks, drafts, and reminders.] [accID: 1]", + "1103 Dan Ames [alt: This chat is with Dan Ames.] [accID: 103]", + "1102 Cyd Ames [alt: This chat is with Cyd Ames.] [accID: 102]", + "1100 Ada Ames [alt: This chat is with Ada Ames.] [accID: 100]", + "2012 #vít-room [rbr] [alt: Acme Holdings • This chat room is for anything #vít-room related.] [sub: Acme Holdings]", + "1111 Lou Ames [alt: This chat is with Lou Ames.] [accID: 111]", + "1109 Jon Ames [alt: This chat is with Jon Ames.] [accID: 109]", + "1108 Ivy Ames [alt: This chat is with Ivy Ames.] [accID: 108]", + "1106 Gus Ames [alt: This chat is with Gus Ames.] [accID: 106]", + "1105 Fay Ames [alt: This chat is with Fay Ames.] [accID: 105]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "mith": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "2001 Alex Smith [alt: alex.smith@example.com] [accID: 14]", + "1015 Alex Smith Jr. [alt: alex.smith2@example.com] [accID: 15]", + ], + "recentReports": [ + "1015 Alex Smith Jr. [alt: This chat is with Alex Smith Jr..] [accID: 15]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "new.person@example.com": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [], + "recentReports": [], + "selfDMChat": null, + "userToInvite": "301623904 new.person@example.com [alt: new.person@example.com] [accID: 301623904]", + "workspaceChats": [], + }, + "nounce": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [], + "recentReports": [ + "2011 #announce [alt: Vít Workspace • This chat is with everyone in Vít Workspace. Use it for the most important announcements.] [sub: Vít Workspace]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "nunez": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "2020 José Ñúñez [alt: jose.nunez@example.com] [accID: 2]", + ], + "recentReports": [ + "2003 Weekend Hiking [alt: This chat is with José Ñúñez, Mary-Jane O'Brien, and Vít Novák.]", + "2020 José Ñúñez [bold] [alt: Packing list] [accID: 2]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "obrien": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "2000 Mary-Jane O'Brien [archived] [alt: obrien@example.com] [accID: 11]", + ], + "recentReports": [ + "2003 Weekend Hiking [alt: This chat is with José Ñúñez, Mary-Jane O'Brien, and Vít Novák.]", + "2000 Mary-Jane O'Brien (archived) [archived] [alt: This chat room has been archived.] [accID: 11]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "oldings": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [], + "recentReports": [ + "2012 #vít-room [rbr] [alt: Acme Holdings • This chat room is for anything #vít-room related.] [sub: Acme Holdings]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "orkspace": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [], + "recentReports": [ + "2013 #orphan-room [alt: Gone Workspace • This chat room is for anything #orphan-room related.] [sub: Gone Workspace]", + "2011 #announce [alt: Vít Workspace • This chat is with everyone in Vít Workspace. Use it for the most important announcements.] [sub: Vít Workspace]", + "2006 Groß Straße's expenses [alt: Vít Workspace • This is where Groß Straße will submit expenses to Vít Workspace. Just use the + button.] [sub: Vít Workspace]", + "2005 Current User's expenses [alt: Vít Workspace • This is where Current User will submit expenses to Vít Workspace. Just use the + button.] [sub: Vít Workspace • Submits to Current User]", + "2010 #admins [alt: Vít Workspace • This chat is with Vít Workspace admin. Use it to chat about workspace setup and more.] [sub: Vít Workspace]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "ovak": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "1003 Vít Novák [alt: vit.novak@example.com] [accID: 3]", + ], + "recentReports": [ + "2003 Weekend Hiking [alt: This chat is with José Ñúñez, Mary-Jane O'Brien, and Vít Novák.]", + "1003 Vít Novák [alt: This chat is with Vít Novák.] [accID: 3]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "packing thread": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [], + "recentReports": [ + "2026 Packing list [bold] [alt: José Ñúñez: Tent and stove]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "rene": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "20 René Dupont [alt: rene@example.com] [accID: 20] [nav: rene@example.com]", + ], + "recentReports": [], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "smith jr": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "1015 Alex Smith Jr. [alt: alex.smith2@example.com] [accID: 15]", + ], + "recentReports": [ + "1015 Alex Smith Jr. [alt: This chat is with Alex Smith Jr..] [accID: 15]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "strasse": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "5 Groß Straße [alt: strasse@example.com] [accID: 5] [nav: strasse@example.com]", + ], + "recentReports": [ + "2007 Groß Straße [alt: Vít Workspace • This chat is for invoices between Groß Straße and Vít Workspace. Use the + button to send an invoice.] [sub: Invoices]", + "2006 Groß Straße's expenses [alt: Vít Workspace • This is where Groß Straße will submit expenses to Vít Workspace. Just use the + button.] [sub: Vít Workspace]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "thread": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [], + "recentReports": [ + "2026 Packing list [bold] [alt: José Ñúñez: Tent and stove]", + "2025 Empty pinned thread [alt: No activity yet]", + "2024 Empty thread with a draft [alt: No activity yet]", + "2021 Thread about the offsite [bold] [alt: José Ñúñez: Count me in]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "to:al": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [], + "recentReports": [], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "vit": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "1003 Vít Novák [alt: vit.novak@example.com] [accID: 3]", + ], + "recentReports": [ + "2012 #vít-room [rbr] [alt: Acme Holdings • This chat room is for anything #vít-room related.] [sub: Acme Holdings]", + "2011 #announce [alt: Vít Workspace • This chat is with everyone in Vít Workspace. Use it for the most important announcements.] [sub: Vít Workspace]", + "2006 Groß Straße's expenses [alt: Vít Workspace • This is where Groß Straße will submit expenses to Vít Workspace. Just use the + button.] [sub: Vít Workspace]", + "2005 Current User's expenses [alt: Vít Workspace • This is where Current User will submit expenses to Vít Workspace. Just use the + button.] [sub: Vít Workspace • Submits to Current User]", + "2003 Weekend Hiking [alt: This chat is with José Ñúñez, Mary-Jane O'Brien, and Vít Novák.]", + "2010 #admins [alt: Vít Workspace • This chat is with Vít Workspace admin. Use it to chat about workspace setup and more.] [sub: Vít Workspace]", + "1003 Vít Novák [alt: This chat is with Vít Novák.] [accID: 3]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "vit workspace": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [], + "recentReports": [ + "2011 #announce [alt: Vít Workspace • This chat is with everyone in Vít Workspace. Use it for the most important announcements.] [sub: Vít Workspace]", + "2006 Groß Straße's expenses [alt: Vít Workspace • This is where Groß Straße will submit expenses to Vít Workspace. Just use the + button.] [sub: Vít Workspace]", + "2005 Current User's expenses [alt: Vít Workspace • This is where Current User will submit expenses to Vít Workspace. Just use the + button.] [sub: Vít Workspace • Submits to Current User]", + "2010 #admins [alt: Vít Workspace • This chat is with Vít Workspace admin. Use it to chat about workspace setup and more.] [sub: Vít Workspace]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "workspace": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [], + "recentReports": [ + "2013 #orphan-room [alt: Gone Workspace • This chat room is for anything #orphan-room related.] [sub: Gone Workspace]", + "2011 #announce [alt: Vít Workspace • This chat is with everyone in Vít Workspace. Use it for the most important announcements.] [sub: Vít Workspace]", + "2006 Groß Straße's expenses [alt: Vít Workspace • This is where Groß Straße will submit expenses to Vít Workspace. Just use the + button.] [sub: Vít Workspace]", + "2005 Current User's expenses [alt: Vít Workspace • This is where Current User will submit expenses to Vít Workspace. Just use the + button.] [sub: Vít Workspace • Submits to Current User]", + "2010 #admins [alt: Vít Workspace • This chat is with Vít Workspace admin. Use it to chat about workspace setup and more.] [sub: Vít Workspace]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "x.com": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "1012 first.last+tag@x.com [alt: first.last+tag@x.com] [accID: 12]", + ], + "recentReports": [ + "1012 first.last+tag@x.com [alt: This chat is with first.last+tag@x.com.] [accID: 12]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "you": { + "currentUserOption": "1 Current User [alt: current.user@example.com] [accID: 1] [nav: current.user@example.com]", + "hasMore": false, + "personalDetails": [ + "1 Current User [alt: current.user@example.com] [accID: 1] [nav: current.user@example.com]", + ], + "recentReports": [ + "2004 Current User (you) [alt: This is your personal space. Use it for notes, tasks, drafts, and reminders.] [accID: 1]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "zzzqqq": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [], + "recentReports": [], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "İSMAIL": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "1004 İsmail Yılmaz [alt: ismail@example.com] [accID: 4]", + ], + "recentReports": [ + "1004 İsmail Yılmaz [alt: Thanks!] [accID: 4]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "δυσσευς": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "1007 Οδυσσεύς [alt: odysseus@example.com] [accID: 7]", + ], + "recentReports": [ + "1007 Οδυσσεύς [alt: This chat is with Οδυσσεύς.] [accID: 7]", + ], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "δυσσευσ": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [], + "recentReports": [], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, + "👩‍💻": { + "currentUserOption": null, + "hasMore": false, + "personalDetails": [ + "8 Zoë 👩‍💻 [alt: zoe@example.com] [accID: 8] [nav: zoe@example.com]", + ], + "recentReports": [], + "selfDMChat": null, + "userToInvite": null, + "workspaceChats": [], + }, +} +`; diff --git a/tests/utils/searchParity/generateSearchFixture.ts b/tests/utils/searchParity/generateSearchFixture.ts new file mode 100644 index 000000000000..93ccb12f262c --- /dev/null +++ b/tests/utils/searchParity/generateSearchFixture.ts @@ -0,0 +1,739 @@ +// cspell:disable -- names and queries deliberately use diacritics, non-Latin scripts and misspelled forms +/* eslint-disable @typescript-eslint/naming-convention -- logins and IDs are object keys throughout this file */ +import type {PrivateIsArchivedMap} from '@hooks/usePrivateIsArchivedMap'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Logins, PersonalDetails, PersonalDetailsList, Policy, Report, ReportActions} from '@src/types/onyx'; +import type {ReportAttributes, SortedReportActionsDerivedValue, VisibleReportActionsDerivedValue} from '@src/types/onyx/DerivedValues'; + +import type {OnyxCollection, OnyxMultiSetInput} from 'react-native-onyx'; + +import Onyx from 'react-native-onyx'; + +import getOnyxValue from '../getOnyxValue'; +import waitForBatchedUpdates from '../waitForBatchedUpdates'; + +const CURRENT_USER_EMAIL = 'current.user@example.com'; + +const POLICY_IDS = { + OWN: 'A1B2C3D4E5F60001', + OTHER: 'A1B2C3D4E5F60002', + MISSING: 'A1B2C3D4E5F60003', +} as const; + +const ACCOUNT_IDS = { + CURRENT_USER: 1, + UNKNOWN: 999, + CONCIERGE: CONST.ACCOUNT_ID.CONCIERGE, + NOTIFICATIONS: CONST.ACCOUNT_ID.NOTIFICATIONS, + JOSE_NUNEZ: 2, + VIT_NOVAK: 3, + ISMAIL_YILMAZ: 4, + GROSS_STRASSE: 5, + BOB_BUILDER: 10, + MARY_JANE_OBRIEN: 11, + ALEX_SMITH: 14, +} as const; + +const SESSION = { + accountID: ACCOUNT_IDS.CURRENT_USER, + email: CURRENT_USER_EMAIL, +}; + +/** Contacts and reports carry fixed IDs, so adding one never renumbers the others and churns the golden master. */ +const FIRST_FILLER_ACCOUNT_ID = 100; + +/** A contact's generated 1:1 DM is DM_REPORT_ID_OFFSET + its accountID. */ +const DM_REPORT_ID_OFFSET = 1000; + +/** The system accounts' real IDs would break the accountID-derived reportID, day and DM assignment, so their DMs are pinned instead. */ +const SYSTEM_DM_SLOTS: Record = { + // Empty (no report actions). Search drops it through the hard-excluded notifications login before the empty-notifications-DM rule in reasonForReportToBeInOptionList runs, so that rule is not pinned here + [ACCOUNT_IDS.NOTIFICATIONS]: {reportID: DM_REPORT_ID_OFFSET + 16, day: 16}, + [ACCOUNT_IDS.CONCIERGE]: {reportID: DM_REPORT_ID_OFFSET + 21, day: 0}, +}; + +/** Every reportID is above this, and a report's timestamp is derived from its distance to it. */ +const REPORT_ID_TIMESTAMP_ORIGIN = 1000; + +/** The values Onyx derives from the fixture, which the app passes to the search next to the raw collections. */ +type SearchFixtureDerivedData = { + reportAttributes: Record; + visibleReportActions: VisibleReportActionsDerivedValue; + sortedReportActions: SortedReportActionsDerivedValue; +}; + +type SearchFixture = { + personalDetails: PersonalDetailsList; + reports: NonNullable>; + reportActions: NonNullable>; + policies: NonNullable>; + privateIsArchivedMap: PrivateIsArchivedMap; + + /** Empty until writeSearchFixtureToOnyx returns what Onyx derived. */ + derived: SearchFixtureDerivedData; + draftComments: NonNullable>; + loginList: Logins; + conciergeReportID: string; +}; + +const NAMED_CONTACTS: Array> = [ + { + accountID: ACCOUNT_IDS.CURRENT_USER, + login: CURRENT_USER_EMAIL, + displayName: 'Current User', + firstName: 'Current', + lastName: 'User', + }, + { + accountID: ACCOUNT_IDS.JOSE_NUNEZ, + login: 'jose.nunez@example.com', + displayName: 'Jos\u00e9 \u00d1\u00fa\u00f1ez', + }, + { + accountID: ACCOUNT_IDS.VIT_NOVAK, + login: 'vit.novak@example.com', + displayName: 'V\u00edt Nov\u00e1k', + }, + { + accountID: ACCOUNT_IDS.ISMAIL_YILMAZ, + login: 'ismail@example.com', + displayName: '\u0130smail Y\u0131lmaz', + }, + { + accountID: ACCOUNT_IDS.GROSS_STRASSE, + login: 'strasse@example.com', + displayName: 'Gro\u00df Stra\u00dfe', + }, + { + accountID: 6, + login: 'lukasz@example.com', + displayName: '\u0141ukasz W\u00f3jcik', + }, + { + accountID: 7, + login: 'odysseus@example.com', + displayName: '\u039f\u03b4\u03c5\u03c3\u03c3\u03b5\u03cd\u03c2', + }, + { + accountID: 8, + login: 'zoe@example.com', + displayName: 'Zo\u00eb \ud83d\udc69\u200d\ud83d\udcbb', + }, + { + accountID: 9, + login: 'ana.maria@example.com', + displayName: 'Ana\u200bMaria', + }, + { + accountID: ACCOUNT_IDS.BOB_BUILDER, + login: 'bob.builder@example.com', + displayName: 'Bob\u00a0Builder', + }, + { + accountID: ACCOUNT_IDS.MARY_JANE_OBRIEN, + login: 'obrien@example.com', + displayName: "Mary-Jane O'Brien", + }, + {accountID: 12, login: 'first.last+tag@x.com', displayName: ''}, + {accountID: 13, login: '+15005550006@expensify.sms', displayName: ''}, + {accountID: ACCOUNT_IDS.ALEX_SMITH, login: 'alex.smith@example.com', displayName: 'Alex Smith'}, + + // Shares the "alex smith" prefix but not the full name, so the two contacts never tie when sorted by name + { + accountID: 15, + login: 'alex.smith2@example.com', + displayName: 'Alex Smith Jr.', + }, + { + accountID: ACCOUNT_IDS.NOTIFICATIONS, + login: CONST.EMAIL.NOTIFICATIONS, + displayName: 'Expensify Notifications', + }, + { + accountID: 17, + login: CONST.ACCOUNT_EXECUTIVE_LOGIN, + displayName: 'Account Executive', + }, + {accountID: 18, login: 'admin@example.com', displayName: 'Domain Admin'}, + {accountID: 19, login: undefined, displayName: 'No Login'}, + + // Stored decomposed (NFD: "e" + combining acute), unlike every other name + { + accountID: 20, + login: 'rene@example.com', + displayName: 'Rene\u0301 Dupont', + }, + { + accountID: ACCOUNT_IDS.CONCIERGE, + login: CONST.EMAIL.CONCIERGE, + displayName: 'Concierge', + }, +]; + +const SEARCH_QUERIES = [ + '', + ' ', + 'a', + 'al', + 'alex smith', + 'ada byrd', + 'smith jr', + 'jr alex', + 'cole lou', + 'vit workspace', + 'ex sm', + 'vit', + 'nunez', + 'ismail', + 'strasse', + 'lukasz', + '\u03b4\u03c5\u03c3\u03c3\u03b5\u03c5\u03c2', + '\ud83d\udc69\u200d\ud83d\udcbb', + 'anamaria', + 'bob builder', + + // Punctuation and phone digits, which a tokenizing search splits differently from a substring match + 'obrien', + 'mary-jane', + 'firstlast', + 'last+tag', + 'x.com', + '@example', + 'rene', + 'concierge', + 'acme', + 'example.com', + 'you', + 'me', + 'hiking', + 'thread', + '\u03b4\u03c5\u03c3\u03c3\u03b5\u03c5\u03c3', + 'admins', + 'workspace', + 'new.person@example.com', + '+1 650 555 0100', + '5550006', + '(500) 555', + + // A padded login: the displayed passes rank by the raw query, which then matches no login exactly + ' alex.smith@example.com ', + 'to:al', + 'zzzqqq', + + // Infixes, which a substring search finds and a word-prefix index does not: in a name, a room name, a workspace subtitle and a group participant's name + 'mith', + 'ikin', + 'nounce', + 'orkspace', + 'oldings', + 'ovak', + + // Matches a thread only through its "Thread" alternate text + 'packing thread', + + // Regular expression metacharacters, which a matcher must treat literally + '*', + '[', + '\\', + '.*', + + // Query-side normalization: case, diacritics (an NFC query against the NFD-stored "René"), ß, a dotted capital I, and an existing contact's phone and the own login, which must not invite + 'V\u00edt', + 'JOS\u00c9', + 'Gro\u00df', + 'Ren\u00e9', + '\u0130SMAIL', + '+15005550006', + 'CURRENT.USER@EXAMPLE.COM', +]; + +const FIRST_NAMES = ['Ada', 'Bea', 'Cyd', 'Dan', 'Eve', 'Fay', 'Gus', 'Hal', 'Ivy', 'Jon', 'Kim', 'Lou']; +const LAST_NAMES = ['Ames', 'Byrd', 'Cole', 'Dunn', 'Eads', 'Ford', 'Gray', 'Hale']; + +// Every filler gets a distinct "First Last" pair (up to FIRST_NAMES.length * LAST_NAMES.length), so no two contacts tie when sorted by name +const FILLER_CONTACT_COUNT = 40; + +/** The derived name of a DM with a nameless phone-number contact. */ +const FORMATTED_PHONE_LOGINS: Record = { + '+15005550006@expensify.sms': '(500) 555-0006', +}; + +/** Per-contact overrides of the generated 1:1 DM, keyed by accountID. */ +const DM_OVERRIDES: Record> = { + // Unread: the contact wrote last and the current user has not read it since + [ACCOUNT_IDS.BOB_BUILDER]: { + lastActorAccountID: ACCOUNT_IDS.BOB_BUILDER, + lastReadTime: '2024-01-01 00:00:00.000', + lastMessageText: 'See you at the trailhead', + }, + + // Read: the current user wrote last and read everything + [ACCOUNT_IDS.ISMAIL_YILMAZ]: { + lastActorAccountID: ACCOUNT_IDS.CURRENT_USER, + lastReadTime: '2024-12-31 00:00:00.000', + lastMessageText: 'Thanks!', + }, +}; + +/** A unique timestamp per report: `day` sets the coarse recency, `sequence` (unique per report, below 3600) breaks every tie. */ +function timestamp(day: number, sequence: number): string { + const minutes = String(Math.floor(sequence / 60) % 60).padStart(2, '0'); + const seconds = String(sequence % 60).padStart(2, '0'); + return `2024-01-${String((day % 28) + 1).padStart(2, '0')} 10:${minutes}:${seconds}.000`; +} + +function buildPolicy(id: string, name: string, role: Policy['role']): Policy { + return { + id, + name, + type: CONST.POLICY.TYPE.TEAM, + role, + owner: CURRENT_USER_EMAIL, + ownerAccountID: ACCOUNT_IDS.CURRENT_USER, + outputCurrency: CONST.CURRENCY.USD, + }; +} + +function participantsOf(accountIDs: number[], hiddenAccountIDs: number[] = []): Report['participants'] { + const participants: NonNullable = {}; + for (const accountID of accountIDs) { + participants[accountID] = { + notificationPreference: hiddenAccountIDs.includes(accountID) ? CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN : CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }; + } + return participants; +} + +/** + * Builds a deterministic fixture covering every searchable report type and edge case (DMs, groups, rooms, threads, tasks, expenses, and status indicators). + * Reports have unique timestamps and contacts have unique names. + */ +function generateSearchFixture(): SearchFixture { + const personalDetails: PersonalDetailsList = {}; + const contactAccountIDs: number[] = []; + + for (const contact of NAMED_CONTACTS) { + personalDetails[contact.accountID] = contact; + contactAccountIDs.push(contact.accountID); + } + for (let index = 0; index < FILLER_CONTACT_COUNT; index++) { + const accountID = FIRST_FILLER_ACCOUNT_ID + index; + const firstName = FIRST_NAMES.at(index % FIRST_NAMES.length) ?? ''; + const lastName = LAST_NAMES.at(Math.floor(index / FIRST_NAMES.length) % LAST_NAMES.length) ?? ''; + personalDetails[accountID] = { + accountID, + login: `${firstName}.${lastName}.${index}@example.com`.toLowerCase(), + displayName: `${firstName} ${lastName}`, + firstName, + lastName, + }; + contactAccountIDs.push(accountID); + } + + const policies: SearchFixture['policies'] = { + [`${ONYXKEYS.COLLECTION.POLICY}${POLICY_IDS.OWN}`]: buildPolicy(POLICY_IDS.OWN, 'V\u00edt Workspace', CONST.POLICY.ROLE.ADMIN), + [`${ONYXKEYS.COLLECTION.POLICY}${POLICY_IDS.OTHER}`]: buildPolicy(POLICY_IDS.OTHER, 'Acme Holdings', CONST.POLICY.ROLE.USER), + }; + + const reports: SearchFixture['reports'] = {}; + const reportActions: SearchFixture['reportActions'] = {}; + const privateIsArchivedMap: Record = {}; + const draftComments: SearchFixture['draftComments'] = {}; + const dmReportIDByAccountID: Record = {}; + + // Real reports always carry a reportName (shouldReportBeInOptionList drops those that don't), so it defaults to the derived name. + const addReport = (reportIDNumber: number, report: Omit, reportName: string, day: number) => { + const reportID = String(reportIDNumber); + const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${reportID}` as const; + if (reports[reportKey]) { + throw new Error(`Two fixture reports share the reportID ${reportID}`); + } + reports[reportKey] = { + reportID, + reportName, + lastVisibleActionCreated: timestamp(day, reportIDNumber - REPORT_ID_TIMESTAMP_ORIGIN), + ...report, + }; + return reportID; + }; + + // Assigns a 1:1 DM to roughly two-thirds of contacts, picked by accountID so adding a contact never moves another contact's DM. + // No contact receives multiple DMs to avoid collection order dependence. + for (const accountID of contactAccountIDs) { + const systemDMSlot = SYSTEM_DM_SLOTS[accountID]; + if (accountID === ACCOUNT_IDS.CURRENT_USER || (!systemDMSlot && accountID % 3 === 2)) { + continue; + } + const contact = personalDetails[accountID]; + const name = contact?.displayName || FORMATTED_PHONE_LOGINS[contact?.login ?? ''] || (contact?.login ?? ''); + dmReportIDByAccountID[accountID] = addReport( + systemDMSlot?.reportID ?? DM_REPORT_ID_OFFSET + accountID, + { + type: CONST.REPORT.TYPE.CHAT, + participants: participantsOf([ACCOUNT_IDS.CURRENT_USER, accountID]), + ...DM_OVERRIDES[accountID], + }, + name, + systemDMSlot?.day ?? accountID % 21, + ); + } + + // The only DM with this contact, so the contact row has exactly one DM to point at. + const archivedDMReportID = addReport( + 2000, + { + type: CONST.REPORT.TYPE.CHAT, + participants: participantsOf([ACCOUNT_IDS.CURRENT_USER, ACCOUNT_IDS.MARY_JANE_OBRIEN]), + }, + "Mary-Jane O'Brien (archived) ", + 25, + ); + privateIsArchivedMap[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${archivedDMReportID}`] = true; + + addReport( + 2001, + { + type: CONST.REPORT.TYPE.CHAT, + participants: participantsOf([ACCOUNT_IDS.CURRENT_USER, ACCOUNT_IDS.ALEX_SMITH], [ACCOUNT_IDS.CURRENT_USER]), + }, + 'Alex Smith', + 26, + ); + + addReport( + 2002, + { + type: CONST.REPORT.TYPE.CHAT, + participants: participantsOf([ACCOUNT_IDS.CURRENT_USER, ACCOUNT_IDS.UNKNOWN]), + }, + '', + 6, + ); + + addReport( + 2003, + { + type: CONST.REPORT.TYPE.CHAT, + chatType: CONST.REPORT.CHAT_TYPE.GROUP, + reportName: 'Weekend Hiking', + participants: participantsOf([ACCOUNT_IDS.CURRENT_USER, ACCOUNT_IDS.JOSE_NUNEZ, ACCOUNT_IDS.VIT_NOVAK, ACCOUNT_IDS.MARY_JANE_OBRIEN]), + }, + 'Weekend Hiking', + 5, + ); + + addReport( + 2004, + { + type: CONST.REPORT.TYPE.CHAT, + chatType: CONST.REPORT.CHAT_TYPE.SELF_DM, + ownerAccountID: ACCOUNT_IDS.CURRENT_USER, + participants: participantsOf([ACCOUNT_IDS.CURRENT_USER]), + }, + 'Current User (you)', + 1, + ); + + // GBR: the current user has something to do in their own workspace chat + addReport( + 2005, + { + type: CONST.REPORT.TYPE.CHAT, + chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + policyID: POLICY_IDS.OWN, + policyName: 'V\u00edt Workspace', + isOwnPolicyExpenseChat: true, + ownerAccountID: ACCOUNT_IDS.CURRENT_USER, + participants: participantsOf([ACCOUNT_IDS.CURRENT_USER]), + hasOutstandingChildRequest: true, + }, + "Current User's expenses", + 7, + ); + + addReport( + 2006, + { + type: CONST.REPORT.TYPE.CHAT, + chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + policyID: POLICY_IDS.OWN, + policyName: 'V\u00edt Workspace', + isOwnPolicyExpenseChat: false, + ownerAccountID: ACCOUNT_IDS.GROSS_STRASSE, + participants: participantsOf([ACCOUNT_IDS.CURRENT_USER, ACCOUNT_IDS.GROSS_STRASSE]), + }, + "Gro\u00df Stra\u00dfe's expenses", + 7, + ); + + addReport( + 2007, + { + type: CONST.REPORT.TYPE.CHAT, + chatType: CONST.REPORT.CHAT_TYPE.INVOICE, + policyID: POLICY_IDS.OWN, + policyName: 'V\u00edt Workspace', + invoiceReceiver: { + type: CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL, + accountID: ACCOUNT_IDS.GROSS_STRASSE, + }, + participants: participantsOf([ACCOUNT_IDS.CURRENT_USER, ACCOUNT_IDS.GROSS_STRASSE]), + }, + 'Gro\u00df Stra\u00dfe', + 12, + ); + + const rooms = [ + [2010, CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, '#admins', POLICY_IDS.OWN, 3, {}], + [2011, CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, '#announce', POLICY_IDS.OWN, 8, {}], + + // RBR: the room has an error + [2012, CONST.REPORT.CHAT_TYPE.POLICY_ROOM, '#v\u00edt-room', POLICY_IDS.OTHER, 14, {errorFields: {joinRoom: {joinRoom: 'Could not join the room'}}}], + [2013, CONST.REPORT.CHAT_TYPE.POLICY_ROOM, '#orphan-room', POLICY_IDS.MISSING, 19, {}], + ] as const satisfies ReadonlyArray]>; + for (const [reportID, chatType, reportName, policyID, day, reportFields] of rooms) { + addReport( + reportID, + { + type: CONST.REPORT.TYPE.CHAT, + chatType, + reportName, + policyID, + policyName: policyID === POLICY_IDS.MISSING ? 'Gone Workspace' : undefined, + participants: participantsOf([ACCOUNT_IDS.CURRENT_USER, ACCOUNT_IDS.JOSE_NUNEZ, ACCOUNT_IDS.ISMAIL_YILMAZ]), + ...reportFields, + }, + reportName, + day, + ); + } + + // Parent chat containing comments from which subsequent threads originate + const threadParentReportID = addReport( + 2020, + { + type: CONST.REPORT.TYPE.CHAT, + participants: participantsOf([ACCOUNT_IDS.CURRENT_USER, ACCOUNT_IDS.JOSE_NUNEZ]), + }, + 'Jos\u00e9 \u00d1\u00fa\u00f1ez', + 3, + ); + const threadParentActions: ReportActions = {}; + reportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${threadParentReportID}`] = threadParentActions; + + // A thread is named after its parent comment, so every thread gets one in the parent DM. + const addThread = (reportIDNumber: number, reportName: string, report: Partial = {}) => { + const parentReportActionID = String(reportIDNumber); + const reportID = addReport( + reportIDNumber, + { + type: CONST.REPORT.TYPE.CHAT, + parentReportID: threadParentReportID, + parentReportActionID, + participants: participantsOf([ACCOUNT_IDS.CURRENT_USER, ACCOUNT_IDS.JOSE_NUNEZ]), + ...report, + }, + reportName, + 4, + ); + threadParentActions[parentReportActionID] = { + reportActionID: parentReportActionID, + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + created: timestamp(3, reportIDNumber - REPORT_ID_TIMESTAMP_ORIGIN), + actorAccountID: ACCOUNT_IDS.JOSE_NUNEZ, + childReportID: reportID, + message: [ + { + type: CONST.REPORT.MESSAGE.TYPE.COMMENT, + html: reportName, + text: reportName, + }, + ], + }; + return reportID; + }; + + const addReply = (threadReportID: string, text: string) => { + reportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${threadReportID}`] = { + '1': { + reportActionID: '1', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + created: reports[`${ONYXKEYS.COLLECTION.REPORT}${threadReportID}`]?.lastVisibleActionCreated ?? '', + actorAccountID: ACCOUNT_IDS.JOSE_NUNEZ, + message: [{type: CONST.REPORT.MESSAGE.TYPE.COMMENT, html: text, text}], + }, + }; + }; + + // Thread with replies to verify that non-empty threads are kept in options + addReply(addThread(2021, 'Thread about the offsite'), 'Count me in'); + + // Hidden for the current user. It has a reply, so the empty-thread rule cannot drop it and only the excludeHidden check that every SearchRouter call site sets keeps it out + addReply( + addThread(2022, 'Hidden thread', { + participants: participantsOf([ACCOUNT_IDS.CURRENT_USER, ACCOUNT_IDS.JOSE_NUNEZ], [ACCOUNT_IDS.CURRENT_USER]), + }), + 'Muted for now', + ); + + // Empty threads are dropped by shouldReportBeInOptionList unless a draft or a pin keeps them + addThread(2023, 'Empty thread'); + const draftedThreadReportID = addThread(2024, 'Empty thread with a draft'); + draftComments[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${draftedThreadReportID}`] = 'unsent draft'; + addThread(2025, 'Empty pinned thread', {isPinned: true}); + + // The only thread whose name lacks "thread", so a query reaches it through its "Thread" alternate text alone + addReply(addThread(2026, 'Packing list'), 'Tent and stove'); + + addReport( + 2030, + { + type: CONST.REPORT.TYPE.TASK, + reportName: 'Book flights', + parentReportID: threadParentReportID, + ownerAccountID: ACCOUNT_IDS.CURRENT_USER, + participants: participantsOf([ACCOUNT_IDS.CURRENT_USER, ACCOUNT_IDS.JOSE_NUNEZ]), + }, + 'Book flights', + 9, + ); + + addReport( + 2031, + { + type: CONST.REPORT.TYPE.EXPENSE, + reportName: 'Offsite expenses', + policyID: POLICY_IDS.OWN, + ownerAccountID: ACCOUNT_IDS.CURRENT_USER, + participants: participantsOf([ACCOUNT_IDS.CURRENT_USER]), + }, + 'Offsite expenses', + 11, + ); + + // Dropped by shouldReportBeInOptionList: an empty submitted expense report whose only action is CREATED. + const emptySubmittedReportID = addReport( + 2032, + { + type: CONST.REPORT.TYPE.EXPENSE, + reportName: 'Empty submitted report', + policyID: POLICY_IDS.OWN, + ownerAccountID: ACCOUNT_IDS.CURRENT_USER, + participants: participantsOf([ACCOUNT_IDS.CURRENT_USER]), + total: 0, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + }, + 'Empty submitted report', + 13, + ); + reportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${emptySubmittedReportID}`] = { + '1': { + reportActionID: '1', + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + created: timestamp(13, 0), + }, + }; + + // Dropped by processReport: neither a room nor any participants. + addReport(2033, {type: CONST.REPORT.TYPE.CHAT, participants: {}}, 'Empty', 2); + + // Dropped by shouldReportBeInOptionList: still loading, so the report itself has no reportName yet. + addReport( + 2034, + { + type: CONST.REPORT.TYPE.CHAT, + chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, + reportName: undefined, + policyID: POLICY_IDS.OWN, + participants: participantsOf([ACCOUNT_IDS.CURRENT_USER, ACCOUNT_IDS.JOSE_NUNEZ]), + }, + '#loading-room', + 27, + ); + + const conciergeReportID = dmReportIDByAccountID[ACCOUNT_IDS.CONCIERGE]; + if (!conciergeReportID) { + throw new Error('The fixture must give the Concierge contact a DM'); + } + + return { + personalDetails, + reports, + reportActions, + policies, + privateIsArchivedMap, + derived: { + reportAttributes: {}, + visibleReportActions: {}, + sortedReportActions: { + sortedActions: {}, + lastActions: {}, + transactionThreadIDs: {}, + }, + }, + draftComments, + conciergeReportID, + loginList: { + [CURRENT_USER_EMAIL]: { + created: '2024-01-01 00:00:00.000', + accountID: ACCOUNT_IDS.CURRENT_USER, + partnerID: 1, + partnerUserID: CURRENT_USER_EMAIL, + lastLogin: '2024-01-01 00:00:00.000', + validatedDate: '2024-01-01 00:00:00.000', + }, + }, + }; +} + +/** Shared by every archived report, so an unchanged archive keeps its reference between writes. */ +const ARCHIVED_REPORT_NAME_VALUE_PAIRS = { + private_isArchived: '2024-01-27 10:00:00.000', +}; + +/** The account's Onyx keys and values, for every key the search reads back. */ +function getOnyxEntries(fixture: SearchFixture): OnyxMultiSetInput { + return { + [ONYXKEYS.SESSION]: SESSION, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: fixture.personalDetails, + [ONYXKEYS.LOGINS]: fixture.loginList, + ...fixture.reports, + ...fixture.reportActions, + ...fixture.policies, + ...fixture.draftComments, + + // Mirrors privateIsArchivedMap so module-level archived reads agree with the map passed to createFilteredOptionList + ...Object.fromEntries(Object.keys(fixture.privateIsArchivedMap).map((key) => [key, ARCHIVED_REPORT_NAME_VALUE_PAIRS])), + }; +} + +async function writeOnyxEntries(entries: OnyxMultiSetInput): Promise { + await Onyx.multiSet(entries); + await waitForBatchedUpdates(); + const [reportAttributes, visibleReportActions, sortedReportActions] = await Promise.all([ + getOnyxValue(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES), + getOnyxValue(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS), + getOnyxValue(ONYXKEYS.DERIVED.RAM_ONLY_SORTED_REPORT_ACTIONS), + ]); + + // Without derived values every report would silently fall back to defaults the app never computed + if (!reportAttributes?.reports || !visibleReportActions || !sortedReportActions) { + throw new Error('Onyx did not derive the report values; call initOnyxDerivedValues before writing the fixture'); + } + return { + reportAttributes: reportAttributes.reports, + visibleReportActions, + sortedReportActions, + }; +} + +/** Writes the whole account to Onyx and returns what Onyx derived from it. */ +function writeSearchFixtureToOnyx(fixture: SearchFixture): Promise { + return writeOnyxEntries(getOnyxEntries(fixture)); +} + +export {generateSearchFixture, writeSearchFixtureToOnyx, ACCOUNT_IDS, CURRENT_USER_EMAIL, POLICY_IDS, SEARCH_QUERIES}; +export type {SearchFixture};